Still using Express.js ? THIS will BLOW your mind
Tags: node, typescript, javascript, webdev
Published: 2023-02-13 17:35:08

Launched in 2010, Express.js made spinning up a server and creating an API endpoint really easy, in just under 15-20 lines of code, you are good to go. Being a minimal and lightweight framework, it was (or still is) the go-to framework for node developers to set up their backend and start creating APIs.

But as time passed, new frameworks emerged and express… will didn’t evolve over time as it should have in fact, its v5 is still in beta even after more than 7 years. So it is time for us to move on from express.js and switch to a different alternative. This article is not a rant about how bad express.js is, or neither do I have any hatred toward express, but to just explore a better alternative with all the features that you usually need to pick and choose while using express.  

## Welcome Adonis.js

So let’s talk about the elephant in the room, Adonis. No, not the Greek god, but ****A fully featured web framework *for* Node.js** that is [Adonis.js](https://adonisjs.com/). Adonis is a batteries-included web framework that follows the MVC pattern and ships with Authentication, Authorization, an SQL ORM, a template engine, a file storage engine, a CLI tool, social auth, and much more! Sounds too good to be true, right? But it actually is that amazing, let’s see how.

## Installation

To install adonis, run the below command 

```bash
npm init adonis-ts-app@latest project-name
```

After that, you will be asked what type of backend are you setting up, `api`, `web`, or `slim`. 

`api` will generate backend-only code and install the packages required for it.

`web` will generate backend, frontend (using edge template engine) and install the required packages.

`slim` will generate the smallest possible AdonisJS application and does not install any additional packages except the framework core.

Below is the folder structure for `web` 

```bash
 ┣ 📂app                  // this contains controllers, middlewares, etc.
 ┃ ┗ 📂Exceptions
 ┃ ┃ ┗ 📜Handler.ts
 ┣ 📂commands
 ┃ ┗ 📜index.ts
 ┣ 📂config               // this is where all the configuration options are
 ┃ ┣ 📜app.ts
 ┃ ┣ 📜bodyparser.ts
 ┃ ┣ 📜cors.ts
 ┃ ┣ 📜drive.ts
 ┃ ┣ 📜hash.ts
 ┃ ┣ 📜session.ts
 ┃ ┣ 📜shield.ts
 ┃ ┗ 📜static.ts
 ┣ 📂contracts
 ┃ ┣ 📜drive.ts
 ┃ ┣ 📜env.ts
 ┃ ┣ 📜events.ts
 ┃ ┣ 📜hash.ts
 ┃ ┗ 📜tests.ts
 ┣ 📂providers
 ┃ ┗ 📜AppProvider.ts
 ┣ 📂public
 ┃ ┗ 📜favicon.ico
 ┣ 📂resources
 ┃ ┗ 📂views
 ┃ ┃ ┣ 📂errors
 ┃ ┃ ┃ ┣ 📜not-found.edge
 ┃ ┃ ┃ ┣ 📜server-error.edge
 ┃ ┃ ┃ ┗ 📜unauthorized.edge
 ┃ ┃ ┗ 📜welcome.edge
 ┣ 📂start
 ┃ ┣ 📜kernel.ts
 ┃ ┗ 📜routes.ts             // this file contains all the route declarations
 ┣ 📂tests
 ┃ ┣ 📂functional
 ┃ ┃ ┗ 📜hello_world.spec.ts
 ┃ ┗ 📜bootstrap.ts
 ┣ 📜.adonisrc.json
 ┣ 📜.editorconfig
 ┣ 📜.env
 ┣ 📜.env.example
 ┣ 📜.env.test
 ┣ 📜.gitignore
 ┣ 📜ace
 ┣ 📜ace-manifest.json
 ┣ 📜env.ts
 ┣ 📜LICENSE
 ┣ 📜package-lock.json
 ┣ 📜package.json
 ┣ 📜server.ts
 ┣ 📜test.ts
 ┗ 📜tsconfig.json
```

So let’s start by looking at all the functionalities one by one.

## Features highlight

Adonis comes with a lot of features built-in and has many first-party packages which makes your job as a developer much much easier, and your DX also increases quite a lot. Let’s look at them one by one

### Feature-rich router

Adonis’s router comes with many features that are not present in express such as grouped routes, prefixing, params matcher, domain lookup (helps when receiving requests from multiple domains/sub-domains), URL signature verifier, etc.

We will cover a few important features here, and you can check the rest on their [docs](https://docs.adonisjs.com/guides/introduction).

Start your development server by using the command `npm run dev` and go to [http://127.0.0.1:3333/](http://127.0.0.1:3333/)

You will find your first route in the `start/routes.ts` file

```tsx
// start/routes.ts

// for "web" setup, this will be the default route
Route.get('/', async ({ view }) => {
  return view.render('welcome')
})

// and

// for "api" setup, this will be the default route
Route.get("/", async ({ response }) => {
  return response.send({
    message: "Hello World",
  });
});
```

You can add more routes one below another.

1. Route group

This is one of my favorite feature, it just automatically makes your routes file clean, readable, and manageable. Using route group, it becomes easy to group your APIs, and by doing so, you can apply middleware to all the routes inside a route group like below

```tsx
// start/routes.ts

Route.group(() => {
  // routes goes here

	// For all the routes in this group, below middleware is applied

}).middleware(async (ctx, next) => {
    console.log(`Inside middleware ${ctx.request.url()}`)
    await next()
  })
```

1. Route prefix

Using this feature API versioning cannot get any easier, better, and more readable, with just a single line of code all of the API endpoints are versioned, and this gives you the power to create the next version of API without any hiccups.

We can also use prefixes to clean your routes file like this

```tsx
// start/routes.ts

Route.group(() => {
	// all users routes
  Route.group(() => {
		Route.post("/", async ({ response }) => { /* route logic goes here*/ }))
		Route.get("/:id", async ({ response }) => { /* route logic goes here*/ }))
		Route.put("/:id", async ({ response }) => { /* route logic goes here*/ }))
		Route.delete("/:id", async ({ response }) => { /* route logic goes here*/ }))
  }).prefix("users");

// all posts routes
Route.group(() => {
		Route.post("/", async ({ response }) => { /* route logic goes here*/ }))
		Route.get("/:id", async ({ response }) => { /* route logic goes here*/ }))
		Route.put("/:id", async ({ response }) => { /* route logic goes here*/ }))
		Route.delete("/:id", async ({ response }) => { /* route logic goes here*/ }))
  }).prefix("posts");
}).prefix("v1");

// now routes in the above group becomes
// http://127.0.0.1:3333/v1/users/REST_OF_THE_ENDPOINT
```

1. Params matcher

Having endpoints with query parameters is a common way of creating APIs, but while integrating APIs with the frontend, one can easily pass the wrong type of parameters, params matcher solves this problem by returning a friendly error message instead of generating unwanted server errors. 

```tsx
Route.get("/:id", async ({ response }) => {
      return response.send({
        message: "Hello World",
      });
    }).where("id", /^[0-9]+$/);
		// ^^^^^^^^^^^^^^^^^^^^^^^ this is where the magic happens
```

### Controllers

As adonis is based on the MVC pattern, Controllers are how you handle HTTP requests, but the main reason why you would want to use them is to de-clutter your routes file and move your business logic into its own separate file so as your backend code grows it is manageable and readable.

You can create a new controller using the CLI command `node ace make:controller Post` it will create a new file in `app/Controllers/Http/` folder, also it is not necessary to keep file in this specific folder you can save it anywhere.

But writing CRUD API routes like this will increase your routes file for no good reason.

```tsx
Route.get("/", "PostsController.index")
Route.get("/:id", "PostsController.show")
Route.post("/:id", "PostsController.store")
Route.put("/:id", "PostsController.update")
Route.delete("/:id", "PostsController.delete")
```

Using [Resources](https://docs.adonisjs.com/guides/controllers#resourceful-routes-and-controllers) function you can do this in just one line, so using our previous example, our code will look like this

```tsx
Route.group(() => {
	// all users routes
  Route.resource('users', 'UsersController')

	// all posts routes
	Route.resource('posts', 'PostsController')

}).prefix("v1");

```

Isn’t that amazing! You can easily replace 5 lines with just one.

Resource function will by default create 7 routes for frontend and backend combined, but you can choose only which one you want like this

```tsx
Route.resource('posts', 'PostsController').except(['create', 'edit'])
```

Now our `posts` resource will generate the rest of the routes except `create` and `edit`. You can check all the routes registered in your application by this simple command `node ace list:routes` and it will print list in this nice format.

```bash
# terminal

GET|HEAD     /v1/posts ───────────────── posts.index › PostsController.index
POST         /v1/posts ───────────────── posts.store › PostsController.store
GET|HEAD     /v1/posts/:id ─────────────── posts.show › PostsController.show
PUT|PATCH    /v1/posts/:id ─────────── posts.update › PostsController.update
DELETE       /v1/posts/:id ───────── posts.destroy › PostsController.destroy
```

### Authentication and Authorization

This is the most repeated and dreaded functionality that is needed in almost every web app, adonis provides a first-party package that provides this functionality while giving total control to the developer. Auth package provides three types of authentication methods, which are session-based auth, API tokens, and **[HTTP basic authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme).**

Install the package using `npm i @adonisjs/auth` and configure the package by running `node ace configure @adonisjs/auth`

### Lucid ORM

Based on **[active record pattern](https://en.wikipedia.org/wiki/Active_record_pattern) lucid ORM supports PostgreSQL**, **MySQL**, **MSSQL**, **MariaDB** and **SQLite. It uses [Knex.js](https://knexjs.org/) under the hook to create and execute SQL queries, lucid also have support for SQL transactions.**

Using lucid ORM database interactions becomes way easier so that you can focus on writing business logic rather than fighting with SQL queries

### Validators

Now that we have done setting up our routes and controllers, we need talk about how are going to handle what type of data that we are getting through requests, there should be a simple way to do this, right? Well there is adonis, with built in validators we are pretty much sorted.

Just import `schema` and write your validators something like this and you are good to go

```tsx
const postSchema = schema.create({
    title: schema.string(),
    body: schema.string(),
    user: schema.string(),
    category: schema.string(),
    tags: schema.array().members(schema.string()),
  })
```

The above code will guarantee that the data we receive from the request will have all those properties and data types correctly.

When used in an example it will look like this

```tsx
// app\Controllers\Http\PostsController.ts

import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from "@ioc:Adonis/Core/Validator";

export default class PostsController {

  public async store({ request,response}: HttpContextContract) {
    const postSchema = schema.create({
      title: schema.string(),
      body: schema.string(),
      user: schema.string(),
      category: schema.string(),
      tags: schema.array().members(schema.string()),
    });

    const data = await request.validate({ schema: postSchema })
    
    // here "data" will be guaranteed to have the necessary values and data types so
    // that you can store it in the database
    
    // database.save(data)

    return response.send({
      message: "Post created",
      status:200
    })
  }
}
```

### Edge templating engine

Adonis ships with its own template for the frontend part, it is quiet simple and doesn’t force you to write code in an opinionated way but rather let’s you add dynamic content by simply using `{{ username }}` two curly braces around a variable. Writing code for edge is like writing html but with some added functions to make it dynamic. Edge has components support like vue, react, or svelte, to reuse code and isolate state.

These were some of the best features of Adonis, and there are plenty more that I haven’t covered in this article (which would have made this article really loooong) that will make your life easier while creating APIs.

Adonis.js, in my opinion is really an amazing choice for the backend, having so many functionalities out of the box makes you focus on creating APIs rather than searching and choosing the right library/package.

Thank you for reading this far, and if you like this article, you can follow me on my socials to get notified when the next article is out, till then goodbye.

#### Social Handles:

Twitter: [pratikb64](https://twitter.com/pratikb64)

Linkedin: [Pratik Badhe](https://www.linkedin.com/in/pratikbadhe/)