How Adonis could be the future-oriented implementation of Node.js

06.07.2021Ricky Elfner
Mobile Adonis.js Node.js Open source JavaScript Framework Hands-on Tutorial How-to

Adonis JS is another framework that we will deal with this week. This is now in version 5.0.

Basics

AdonisJS was started in 2015 as an Open Source project and was intended to serve as an alternative framework for the Node.js ecosystem. The advantage of Adonis is that it brings various basic functions with it. Therefore, you do not have to use different libraries to use these functions.

This includes, for example, an SQL ORM, including Query Builder, migrations and models. Also included are different HTTP Routing features, form validation, and Health Checks modules, which can be used with Kubernetes.

Project structure

With the introduction of version 5.0, new projects are now created directly via npm init or yarn create. The use of TypeScript is also new.

When creating a new project, you have the option of choosing between the two project structures API Server or Web Application. If you opt for a full-fledged server-side web application, an AdonisJS template engine, the session module and the module for web security & CSRF protection are also installed in addition to the core functions.

However, if you decide to use the API server, only the most essential things that are required for a JSON API server are installed here.

Authentication

An authentication system is provided to you directly via the Adonis Framework. This enables you to protect your web application without great effort in combination with a database. The focus here is not on styling, but Adonis would like to make this function available to other systems via the various APIs.

Databases

AdonisJS is one of the few frameworks that provides direct support for SQL databases. A wide variety of tools are made available via the database layer Lucid in order to create data-based applications quickly and easily.

Query Builder

With the help of the API, simple queries and complex join queries can be created quickly and easily.

1
2
3
4
5
6
const user = await Database
  .from('users')
  .select('*')
  .where('username', 'virk')
  .innerJoin('profiles', 'users.id', 'profiles.user_id')
  .first()

Data Models

Data models each represent a table in a database, this makes it easier for you to use data from them. For example, if you have a User table, create a model with the same name. This allows you to use the table directly in your code, as you can see in the example below. This also makes your application a lot clearer.

1
const user = await User.query().where('username', 'virk').first()

Schema migrations

Schema migrations are fundamentally there so that you can create and edit database tables. The great advantage of doing this via the program code is, among other things, that you can also bind the changes to deployment workflows and thus save yourself manual adjustments.

Seeds & Factories

During the development of your application you usually have the problem that you need sample data. This can be awkward depending on how you handle this. With Seeds & Factories you can simplify this process. A seeder is used to fill the database with the data that you have specified manually or with data that you have automatically generated via a Factorie.

Views & Templates

In this framework, Edge is used as the template engine. Like most engines, it supports things like: conditionals, loops, components, runtime debugging and stack traces.

preparation

To use Adonis JS version 5.0 you need:

  • Node.js> = 14.5.0

  • npm> = 6.0.0

PostgresSQL DB

To be able to carry out this tutorial you first need a PostgreSQL DB. If you have not yet installed Postgres, you can do this using, for example, brew if you are using a Mac.

1
brew install postgresql

Once the installation is complete, you can verify this using the command postgres -V from the terminal

To be able to continue now you have to start the PostgreSQL service.

1
brew services start postgresql

You should then create a new user with restricted rights. To do this, first connect to the postgres DB. Then you can execute the Create command and decide on a user name and a desired password. You will need this access data later within the web application. When you have executed the command, you can display all users with \du and thus check whether your newly created user is there.

1
2
psql postgres;
CREATE USER [User Name] WITH PASSWORD '[Password]' CREATEDB;

Now that you have a new user, you can log in with the user and create the DB that you will need later for the DB entries.

1
2
psql postgres -U [User Name]
CREATE DATABASE adonistodo;

You can then display all databases with \l. Now you can also log out with \q and log in to your new DB.

1
psql adonistodo -U [User Name]

With \dt you can also display all schemas, this will help you later to check whether all data was created correctly.

The application

The setup

To create a new project, you have to enter the following command via the terminal. The last parameter determines the name.

1
npm init adonis-ts-app adonisjs-todo

As soon as you have executed this command, you have to decide between the two project structures API Server and Web Application. In our tutorial we will create a web application. You can also change the project name again in this step and decide whether the setup for ESLit and Prettier should be carried out. The installation of the dependencies then starts.

Now you can switch to the directory of your app and open VS Code with code ..

The use of the database

As you have already seen in the preparation, a PostgreSQL database is used for the todo list in this tutorial. Therefore, you now have to install Lucid via the terminal.

1
npm i @adonisjs/lucid@alpha

Once this is done, the setup for Lucid still needs to be done. You have the option of selecting your desired DB (PostgreSQL). You start this process with the following command:

1
node ace invoke @adonisjs/lucid

This will create the default configuration files and register @adonisjs/lucid within the providers array. In addition, you have to add the following variables to the env.ts file in this step:

1
2
3
4
5
PG_HOST: Env.schema.string({ format: 'host' }),
PG_PORT: Env.schema.number(),
PG_USER: Env.schema.string(),
PG_PASSWORD: Env.schema.string.optional(),
PG_DB_NAME: Env.schema.string(),

Another adjustment must be made in the file .env so that your web application can or can access the DB.

1
2
3
4
5
PG_HOST=localhost
PG_PORT=5432
PG_USER=ricky
PG_PASSWORD=test123P
G_DB_NAME=adonistodo

In order to really be able to use the database with Node, it is necessary to install the appropriate driver.

1
npm i pg

Migration

Next, you’ll create the schema for the tables you want for your database. Since this app is a todo list, a table is definitely required for the todo entries. You can have the file for this created via node and then add the desired attributes.

1
node ace make:migration todos

If this worked you will get the message:

1
 CREATE: database/migrations/1618753413969_todos.ts

We complete the function up with the attributes content and complete:

1
2
3
4
5
6
7
8
public async up () {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.string("content");
      table.integer("complete").defaultTo(0);
      table.timestamps(true)
    })
  }

Before you can actually carry out the migration, the project must first be rebuilt. You can do this by starting the server:

1
node ace serve --watch

As soon as the server is up and running, you can start the migration. If this process has been successfully completed, you will also receive a confirmation directly.

1
2
node ace migration:run
❯ migrated database/migrations/1618753413969_todos

In your terminal for the Postgres DB you can now connect to the database adonistodo and display the list of relations with \dt. You will now see todos there. This way you can be sure that everything worked.

Models

Now we switch back to the terminal for the actual application and create a model for the todo items. There is also a command for this directly from node.

1
2
node ace make:model Todo
❯ CREATE: app/Models/Todo.ts

In this file we also determine the two attributes content and complete, with the associated data types.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
export default class Todo extends BaseModel {
  @column({ isPrimary: true })
  public id: BigInt

  @column()
  public content: String

  @column()
  public complete: Number

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime
}

Seeder

In order to fill the database with sample data, there is the possibility to create different seeders under AdonisJs.

1
2
node ace make:seeder Todos
❯ CREATE: database/seeders/Todo.ts

You can now specify the required sample data in the newly created file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
export default class TodoSeeder extends BaseSeeder {
  public async run () {
    await Todo.createMany([
      {
        content:'Test Content 1',
        complete:0,
      },
      {
        content:'Test Content 2',
        complete:0,
      },
      {
        content:'Test Content 3',
        complete:0,
      }
    ])
  }
}

In order to fill the DB with it, you first have to install Luxon for the DateTime format.

1
npm install --save luxon

Now you can really fill the DB:

1
2
node ace db:seed
❯ completed database/seeders/Todo

As soon as you switch to your database within your terminal, you can now see your data.

1
SELECT * FROM todos;

The controllers

Now that you have data and a server that can be started without problems, it is now a matter of the actual functions that your server or your application should have. In our case, you only need a controller that contains the business login. This can also be created automatically via the terminal.

1
2
node ace make:controller TodoController
❯ CREATE: app/Controllers/Http/TodosController.ts

Within this controller you need the method index to start with, which simply queries all existing to-do items from the database. The view list is called as the return value and the previously queried to-do items are transferred as data.

And another method is show, which returns a certain todo item from the DB. This calls the view todo_item and transfers the item found as data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public async index (ctx: HttpContextContract) {
    const todos = await Todo.all()
    return ctx.view.render('list', {todos})
}

public async show({view, params}: HttpContextContract)
{
    try {
        const todo = await Todo.find(params.id);
        return view.render('todo_item', {todo});
    } catch (error) {
        console.log(error)
    }
}

Routes

Within the file routes.ts, create the different URLs. At the same time, you can decide which controllers and methods can be called within this page.

For the beginning the function index is used under the url http://127.0.0.1:3333/todos. The second URL http://127.0.0.1:3333/list/[todo item id] uses the show function and uses the ID in the link to display the corresponding item.

1
2
Route.resource('todos', 'TodosController').only(['index']).apiOnly();
Route.resource('list', 'TodosController').only(['show']).apiOnly();

Views

In order to be able to use the views that have been specified within the controller, you must of course create them.

1
2
node ace make:view list
❯ CREATE: resources/views/list.edge

To get started, you can now simply display all existing todo items using a foreach loop.

1
2
3
@each (todo in todos)
    {{todo.content}} - {{todo.id}}
@endeach

You can call up the specified address in your browser so that you can see your items: http://127.0.0.1:3333/todos.

The second template todo_item is created for the second view.

1
2
node ace make:view todo_item
❯ CREATE: resources/views/todo_item.edge

For the beginning we only want to display the text of the item there.

1
{{todo.content}}

You can also use the URL http://127.0.0.1:3333/list/1 to call up the todo item with the ID “1”.

Adding items

Now we come to the first basic function of a todo list, adding new entries. To do this, you first need a new function within the TodoController that performs this task.

First a new todo object is created for this. The ID is determined based on the current time so that it remains unique. The content is queried from the input field with the name content via the parameter request. The complete status is set to 0 by default. The item can then be saved in the DB. As soon as this is done, you will be directed to the page on which the individual item is displayed.

1
2
3
4
5
6
7
8
9
public async add({ request, response }: HttpContextContract) {
      const todo = new Todo();
      todo.id = BigInt(Date.now());
      todo.content = request.input("content")
      todo.complete = 0;

      await todo.save();
      response.redirect("/list/" + todo.id);
  }

You must also define new routes for this range of functions. If the url is called with /add, you will be redirected to the start page. If a post is carried out, the function add is called from the TodosController.

1
2
Route.on("add").render("welcome");
Route.post("add", "TodosController.add");

As a last step for the functionality you need an input field and a button within the template list.edge

1
2
3
4
5
6
<div class="formArea">
    <form class="form" action="{{ route('TodosController.add') }}" method="post">
        <input class="itemInput" type="text" name="content" value="{{ flashMessages.get('content') || '' }}" />
        <button class="button" type="submit">Add</button>
    </form>
</div>

Now you can create your own new items.

Deleting items

Of course, you also want to be able to delete items. Again, three steps are necessary here. First you create a function within the TodosController. With the ID that is transferred, the corresponding item is searched for and deleted immediately. As soon as this is successful, you will be redirected to the Todos page.

1
2
3
4
5
public async delete({response, request}: HttpContextContract)
    {
        await Todo.query().where('id', request.input('del')).delete();
       return response.redirect('/todos');
    }

In addition, the structure of the two routes is the same, with the difference that it is the Delete function.

1
2
Route.on("delete").render("welcome");
Route.post("delete", "TodosController.delete");

To delete an item, you still need a button, which you can click to delete an item.

1
2
3
<form class="button" action="{{ route('TodosController.delete') }}" method="post">
    <button class="button" type="submit" value="{{todo.id}}" name="del">X</button>
</form>

Checking todo entries

And last but not least, the same process a third time. The method done is created here, which first searches for the item using the ID taken from the Request parameter. If a matching todo item is found, the status is reversed. This differentiates whether the item is done or is still outstanding.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    public async done({response, request}: HttpContextContract)
    {
        const todo = await Todo.find(request.input('done'));

        if(todo){
            todo.complete = todo.complete == 0 ? 1 : 0    
            await todo.save();
        }
        
        return response.redirect('/todos');
    }

Then two new routes have to be created.

1
2
Route.on("done").render("welcome");
Route.post("done", "TodosController.done");

And so that something changes in the styling, we have to add status-dependent stylings. We also have to add a button that changes the status.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<div class="listArea">
    @each (todo in todos)
        <div class="itemarea">
            <div class="listItem" key={{todo.id}}>
            <p style="{{ todo.complete > 0 ? 'text-decoration: line-through' : '' }}">
                {{todo.content}} - {{ todo.complete }}
            </p>
            </div>
            <form class="doneForm" action="{{ route('TodosController.done') }}" method="post">
                <button class="button" type="submit" value="{{todo.id}}" name="done">Done</button>
            </form>
            <form class="deleteForm" action="{{ route('TodosController.delete') }}" method="post">
                <button class="button" type="submit" value="{{todo.id}}" name="del">X</button>
            </form>
        </div>
    @endeach
</div>

You have now managed to create a web application with the Adonis JS framework. Our other ToDo tutorials that we have already created with Angular, React & Vue.js could also be of interest to you. If you are interested, this is the perfect opportunity to learn about the differences between the frameworks.


This text was automatically translated with our golang markdown translator.

Ricky Elfner

Ricky Elfner – Denker, Überlebenskünstler, Gadget-Sammler. Dabei ist er immer auf der Suche nach neuen Innovationen, sowie Tech News, um immer über aktuelle Themen schreiben zu können.