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
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
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",
});
});
You can add more routes one below another.
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();
});
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
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
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");
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");
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"]);
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
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()),
});
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,
});
}
}
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