If you’re just starting out creating applications with your computer, you need to install Node.js. You can read through how to install and set them up properly here.
We are going to demonstrate how to use AdonisJS to create a simple Todo application.
In this AdonisJS tutorial, we are going to explore how to set up AdonisJS, how to explore AdonisJS Requests and Responses, we will also discuss the Controller component and how it interacts with Models and Views.
Setting up AdonisJS
A lot has changed with AdonisJS since the release of AdonisJS 5, but since we are probably starting out with AdonisJS, we will only focus on AdonisJS 5.
But you can review the previous version here.
It is worth noting that as of the time of writing this tutorial, AdonisJS 5 is currently at the preview stage.
AdonisJS requires Node.js 12.x.x and NPM 6.x.x, so you should check the version of your Node.js to make sure it corresponds with the requirement.
Now you can create a new AdonisJS 5 project by simply running this command.
npm init adonis-ts-app adonisjs-todo
yarn create adonis-ts-app adonisjs-todo
If you’re asked to choose the type of web application, select Web application and press Enter on the other options.
The Web application comes with @adonisjs/view, @adonisjs/session and @adonisjs/shield which is essentially what a traditional web application should have.
I will develop a complete tutorial on Building a RESTful API with AdonisJS or you can take the Node. Js: REST APIs Development with AdonisJs course.
Next, change your directory to the current adonisjs-todo folder you just created and open it with any code editor of your choice (Using VSCode).
Lastly, start your development server to see your first AdonisJS web application.
The serve command just starts our HTTP server and perform all the conversion and compilation of TypeScript to JavaScript.
Additionally, the --watch flag simply watches our files for changes.
Visit http://127.0.0.1:3333 in your browser to test your newly created AdonisJS project.
If you see this page:

Cheers!
Setting up Database
Next, we are going to configure our database to communicate with our AdonisJS project.
Setting up a database in AdonisJS is a little different from a traditional way of doing it in other frameworks.
Firstly, you need to install a package called Lucid, which is a powerful ORM used by AdonisJS.
npm i @adonisjs/lucid@alpha
yarn add @adonisjs/lucid@alpha
Next, you need to run the invoke command and choose your database type to set up Lucid and default configurations.
node ace invoke @adonisjs/lucid
For set up instructions, choose any of one and it will be generated and preview for you.
Next, create a new database using any Database Client of your choice and note the login credentials.
Next, open the .env file (or create a new one if not exists), update the following information.
DB_CONNECTION=mysql
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER= //DB_USER
MYSQL_PASSWORD= //DB_PASSWORD
MYSQL_DB_NAME= //DB_NAME
You can always go to config/database.ts to configure some credentials, for this article, we will stick with the defaults.
Setting up Schema
The next step is to set up your database schemas and create the different database relationships that are required.
Let’s see how we can achieve this easily with Lucid:
To create our first schema for Users, we will run the following command.
node ace make:migration users
The command will generate a schema file inside database/migrations, let’s open it and add the following columns to it.
import BaseSchema from "@ioc:Adonis/Lucid/Schema";
export default class Users extends BaseSchema {
protected tableName = "users";
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments("id");
table.string("email").unique().notNullable();
table.string("password").notNullable();
table.string('remember_me_token').nullable()
table.timestamps(true);
});
}
public async down() {
this.schema.dropTable(this.tableName);
}
}
We will repeat the same for the Todo schema and add the following columns to it too.
import BaseSchema from "@ioc:Adonis/Lucid/Schema";
export default class Todos extends BaseSchema {
protected tableName = "todos";
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments("id");
table.string("title");
table.text("desc").nullable();
table.integer("status").defaultTo(0);
table.integer("user_id");
table.timestamps(true);
});
}
public async down() {
this.schema.dropTable(this.tableName);
}
}
Lastly, before you migrate the database, note that you can set up Database Seeders to generate fake data for your database, or clone my repository since I have configured database seeders already.
Now, stop the server and start it again before running the migration:
node ace serve --watch
// Then
node ace migration:run
Set up User Authentication
AdonisJS comes with a full-fledged authentication system using either basic, token, or traditional sessions, but unlike other frameworks, AdonisJS allows the developer to create and customize the register and login pages using any stack of their choices.
Let’s dive into how to set up authentication with AdonisJS:
Install Auth Package
Firstly, running any the command to install the Auth package.
npm i @adonisjs/auth@alpha
yarn add @adonisjs/auth@alpha
Next, run this command to invoke and set up the Auth package.
node ace invoke @adonisjs/auth
The command will prompt you to select the database provider and guard to be used, in the case of this tutorial, since we are building a web app.
We select Lucid and Web. Then type in the name of the model you want to use for authentication, in my case, we will type in User.
Make sure to register the Auth package to the start/kernel.ts file.
Server.middleware.registerNamed({
auth: "App/Middleware/Auth",
});
Now, you can choose to set up the user’s schema here, but we have already done that, so I will ignore it.
Install Session Package
Next, install the @adonisjs/session since we are building a web app, we will use it for authentication.
npm i @adonisjs/session@alpha
yarn add @adonisjs/session@alpha
Install Shield Package
Next, install the @adonisjs/shield for CSRF protection.
npm i @adonisjs/shield@alpha
yarn add @adonisjs/shield@alpha
Run this command to invoke and set up the Shield package.
node ace invoke @adonisjs/shield
Follow the instructions provided and add the package to the start/kernel.ts file.
Server.middleware.register([
"Adonis/Core/BodyParserMiddleware",
"Adonis/Addons/ShieldMiddleware",
]);
Now. open your start/routes.ts file and add the following routes for user registration.
Route.on('register').render('register')
Route.post('register', 'AuthController.register')
Route.get("/dashboard", async ({ auth }) => {
const user = await auth.authenticate();
return `Hello user! Your email address is ${user.email}`;
});
Route.on("login").render("login");
Route.post("/login", "AuthController.login");
Next, make the authentication controller by running this command:
node ace make:controller Auth
The command will create a controller file in app/Controllers/Http, open it and put the following lines of code.
import User from "App/Models/User";
import { schema, rules } from "@ioc:Adonis/Core/Validator";
import { HttpContextContract } from "@ioc:Adonis/Core/HttpContext";
export default class AuthController {
public async register({ request }: HttpContextContract) {
const validationSchema = schema.create({
email: schema.string({ trim: true }, [
rules.email(),
rules.unique({ table: "users", column: "email" }),
]),
password: schema.string({ trim: true }, [rules.confirmed()]),
});
const userDetails = await request.validate({
schema: validationSchema,
});
const user = new User();
user.email = userDetails.email;
user.password = userDetails.password;
await user.save();
await auth.login(user);
response.redirect("/dashboard");
}
public async login({ auth, request, response }: HttpContextContract) {
const email = request.input("email");
const password = request.input("password");
await auth.attempt(email, password);
response.redirect("/dashboard");
}
}
Next, let’s create a Register and Login View for our registration form by running this command.
node ace make:view register
node ace make:view login
A register.edge file will be created at resources/views, open it and put in the following codes.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register</title>
</head>
<body>
<form action="{{ route('AuthController.register') }}" method="post">
<div>
<label for="email">Email</label>
<input type="text" name="email" value="{{ flashMessages.get('email') || '' }}" />
<p>{{ flashMessages.get('errors.email') || '' }}</p>
</div>
<div>
<label for="password">Password</label>
<input type="password" name="password" />
<p>{{ flashMessages.get('errors.password') || '' }}</p>
</div>
<div>
<label for="password_confirmation">Re-Enter Password</label>
<input type="password" name="password_confirmation" />
<p>{{ flashMessages.get('errors.password_confirmation') || '' }}</p>
</div>
<div>
<button type="submit">Create Account</button>
</div>
</form>
</body>
</html>
Open the login.edge file in resources/views and add the following codes:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
</head>
<body>
<form action="{{ route('AuthController.login') }}" method="post">
<div>
<label for="email">Email</label>
<input type="text" name="email" value="{{ flashMessages.get('email') || '' }}" />
<p>{{ flashMessages.get('auth.errors.uid') || '' }}</p>
</div>
<div>
<label for="password">Password</label>
<input type="password" name="password" />
<p>{{ flashMessages.get('auth.errors.password') || '' }}</p>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
</body>
</html>
You can now visit /register and /login to test out your application.
If you encounter any error saying Cannot find module 'phc-argon2' please run the following command to install the package.
If you’re able to register and login successfully, Congratulations.
Creating Models
The app/Models folder will contain all the models that will be created in the course of developing your application.
If you look inside the folder and see a User.ts file inside, that’s exactly what a model is:
Just a file that contains the properties and methods to interact with our database schema, for example, inside the User.ts file, you will notice that it inherits the BaseModel an object that contains all the methods and properties to interact with our Database Schema.
The model can also do a lot more than interacting with Database Schema, we can define relationships, configure our model, etc.
We can use the User model to retrieve or create a new user in our application by providing the data needed in the User schema at database/migrations/xxxx_users.ts file.
Let’s take a look:
To retrieve all the users in our database, we will simply do:
const users = await User.all();
// SQL: SELECT * from "users" ORDER BY "id" DESC;
To retrieve a particular user based on ID:
const user = await User.find(1)
// SQL: SELECT * from "users" WHERE "id" = 1 LIMIT 1;
Create a new User (No need for SQL insert statement)
const user = new User()
user.name = 'Solomon Eseme'
user.email = '[email protected]'
user.password = 'password'
await user.save()
The user.save() method will perform the insert query and save the user to the database.
To delete a User from your Database, simply run:
const user = await User.findOrFail(1)
await user.delete()
Now that we have a glimpse of what AdonisJS Models are, let’s create our own Todo Model to interact with the Todo schema we create above.
Open your project terminal and run the following command.
The command will generate a new Todo Model inside app/Models folder.
Open the file and paste in the following code:
import { DateTime } from "luxon";
import { BaseModel, column, belongsTo, BelongsTo } from "@ioc:Adonis/Lucid/Orm";
import User from "App/Models/User";
export default class Todo extends BaseModel {
@column({ isPrimary: true })
public id: number;
@column()
public title: string;
@column()
public desc: string;
@column()
public status: number;
@column()
public userId: number;
@belongsTo(() => User)
public user: BelongsTo<typeof User>;
@column.dateTime({ autoCreate: true })
public createdAt: DateTime;
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime;
}
The code above simply maps the different columns we have in our database todos table and also defines a revised one to many relationship to our User model.
Creating Controllers
Again, controllers are like the middleman between requests (views) and models.
When a user sends a request to your backend either by clicking a button or submitting a form, the request passes through the routes to the controller and the controller calls out to your model to serve the request and returns a response back to the user (View).
With this flow in mind, let’s create our first controller to handle any request for the Todos:
Run the following command in your project terminal to create a new controller.
node ace make:controller Todo
The command will create a new controller inside app/Controllers/Http/TodosController.ts, open it and paste in the following codes.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Todo from "App/Models/Todo";
export default class TodosController {
public async index({view, request}: HttpContextContract)
{
const todos = await Todo.query().preload('user');
return view.render('dashboard' {todos});
}
public async byUserId({view, auth, request}: HttpContextContract)
{
const user = await auth.authenticate();
await user.preload('todos')
const todos = user.todos
return view.render('dashboard', {todos});
}
public async show({view, request, params}: HttpContextContract)
{
try {
const todo = await Todo.find(params.id);
await todo.preload('user')
return view.render('show', {todo});
} catch (error) {
console.log(error)
}
}
public async edit({view, request, params}: HttpContextContract)
{
const todo = await Todo.find(params.id);
await todo.preload('user')
return view.render('edit', {todo});
}
public async update({view, auth, request, params}: HttpContextContract)
{
const todo = await Todo.find(params.id);
if (todo) {
todo.title = request.input('title');
todo.desc = request.input('desc');
todo.status = request.input('status') == 'on' ? 1 : 0;
if (await todo.save()) {
await todo.preload('user')
return view.render('show', {todo});
}
return;
}
return;
}
public async create({view, request}: HttpContextContract)
{
return view.render('add');
}
public async store({view, auth request, response}: HttpContextContract)
{
const user = await auth.authenticate();
const todo = new Todo();
todo.title = request.input('title');
todo.desc = request.input('desc');
await user.related('todos').save(todo))
response.redirect('/todos/'+todo.id);
}
public async destroy({response, auth, request, params}: HttpContextContract)
{
const user = await auth.authenticate();
const todo = await Todo.query().where('user_id', user.id).where('id', params.id).delete();
return response.redirect('/dashboard');
}
}
The code above just perform a simple CRUD operation using the Todo model and render the different Views will be creating soon.
Also note that the controller does not include any Validation or proper error handling, I just wanted to keep it as basic as possible.
Creating Routes
Next, we are going to create routes that map user’s requests to controller’s methods based on specific user requests.
Now, open the start.ts file inside the start folder and add the following codes:
import Route from "@ioc:Adonis/Core/Route";
Route.on("/").render("welcome");
Route.on("register").render("register");
Route.post("register", "AuthController.register");
Route.group(() => {
Route.get("/dashboard", "TodosController.index").as("dashboard");
Route.get("/todos/user", "TodosController.byUserId");
Route.resource("todos", "TodosController");
}).middleware("auth");
Route.on("login").render("login");
Route.post("/login", "AuthController.login");
Route.post("/logout", "AuthController.logout").as("logout");
When a user clicks on a button or submits a form, how does AdonisJS know which method to call or which controller to send the request to?
Well, everything is defined and mapped using a Routing System.
Creating Views
Views represent how the Information is displayed, it is used for all the UI logic of the software. You are right if you say that the View represents the Frontend of your web page.
To create a View, run the following command:
node ace make:view dashboard
That command will create a dashboard.edge file inside resources/views, open it and paste in the following codes.
@layout('layouts/app')
@section('main')
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<a href="/todos/create" class="btn btn-primary">Add new todo</a>
</div>
</div>
</div>
</div>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<div class="panel-bod">
<table class="table">
<thead>
<th>All Todos</th>
<th>
<a class="btn btn-warning" href="/todos/user">Show mine</a>
</th>
<th>
<a class="btn btn-info" href="/dashboard">Show All</a>
</th>
</thead>
<tbody class="max-w-full">
@each(todo in todos)
<tr class="max-w-full">
<td class="pt-5 pb-5 pr-5">
<h1 class="sm:font-bold">{{ todo.title }}</h1>
<p>{{ todo.desc }}</p>
<p class="pt-2 text-gray-500"> Added By: {{ todo.userId == auth.user.id? 'You':todo.user.name }}</p>
</td>
<td>
<div>
<a href="/todos/{{todo.id}}" class="bg-green-500 btn btn-success">View</a>
@if(todo.userId == auth.user.id)
<a href=" /todos/{{todo.id}}/edit" class="bg-yellow-500 btn btn-primary">Edit</a>
<form class="" method="POST" action="/todos/{{todo.id}}?_method=DELETE">
{{ csrfField() }}
<button class="bg-red-500 btn btn-danger">Delete</button>
</form>
@endif
</div>
</td>
</tr>
@endeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@endsection
There are obviously, a lot more views that have been created to complete this project, such as add.edge, show.edge, edit.edge etc.
You can get access to the repository and clone it.
Preview
If you get everything correctly, you should be presented with a dashboard like this:

Congratulations on making it this far!
When it comes to learning and mastering AdonisJS, this course Learn AdonisJs: from zero to deploy is my top recommendation. You will Learn AdonisJs by building a production-ready application completely from scratch.
Take a break and subscribe to get access to our free NODEJS TIPS that will improve your productivity.