DEV Community

Cover image for Still using Express.js ? THIS will BLOW your mind
Pratik Badhe
Pratik Badhe

Posted on • Originally published at pratikbadhe.com

Still using Express.js ? THIS will BLOW your mind

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. 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

npm init adonis-ts-app@latest project-name
Enter fullscreen mode Exit fullscreen mode

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

 ┣ 📂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
Enter fullscreen mode Exit fullscreen mode

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.

Start your development server by using the command npm run dev and go to http://127.0.0.1:3333/

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

// 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",
  });
});
Enter fullscreen mode Exit fullscreen mode

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

// 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()
  })
Enter fullscreen mode Exit fullscreen mode
  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

// 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
Enter fullscreen mode Exit fullscreen mode
  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.

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

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.

Route.get("/", "PostsController.index")
Route.get("/:id", "PostsController.show")
Route.post("/:id", "PostsController.store")
Route.put("/:id", "PostsController.update")
Route.delete("/:id", "PostsController.delete")
Enter fullscreen mode Exit fullscreen mode

Using Resources function you can do this in just one line, so using our previous example, our code will look like this

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

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

}).prefix("v1");

Enter fullscreen mode Exit fullscreen mode

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

Route.resource('posts', 'PostsController').except(['create', 'edit'])
Enter fullscreen mode Exit fullscreen mode

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.

# 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
Enter fullscreen mode Exit fullscreen mode

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.

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 lucid ORM supports PostgreSQL, MySQL, MSSQL, MariaDB and SQLite. It uses Knex.js 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

const postSchema = schema.create({
    title: schema.string(),
    body: schema.string(),
    user: schema.string(),
    category: schema.string(),
    tags: schema.array().members(schema.string()),
  })
Enter fullscreen mode Exit fullscreen mode

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

// 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
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Linkedin: Pratik Badhe

Top comments (0)