August 17, 2018

Build a Task List with Authentication Using SQL, Node.js and Adonis (Part 1)

by Connor Leech

Build a Task List with Authentication Using SQL, Node.js and Adonis

Introduction

Adonis.js is an MVC framework for Node.js. It is actively built and maintained. The 4.0 release is fast approaching, but in this tutorial we will use the latest stable version (3.0).

In this tutorial, we’re going to build a task application where guests can view all the tasks and click in to view an individual task. We will add authentication functionality using Adonis' built-in tools and design patterns. After adding authentication, users will be able to register with their email and password, logout and create new tasks.

This will be a two part tutorial. In this first part, we will focus on installation, initial templating, creating a local database, adding authentication, and seeding data.

Adonis has built-in connections for many SQL databases, including Postgres and SQLite. In this tutorial, we will store our tasks, users and sessions in MySQL on our local machines.

Adonis borrows heavily from the Laravel PHP framework and borrows concepts from Ruby On Rails. Specifically, it takes the Model View Controller (MVC) pattern of development and provides an Object Relational Mapper (ORM) for database queries. This means that you can quickly scaffold powerful applications in a structured way, instead of having to choose for yourself from thousands of separate npm packages.

If you are new to ES6 or MVC frameworks, that's okay. We'll take this one step at a time!

tl;dr - Source code is on GitHub

Installation and application setup

Adonis.js comes with a command line interface (CLI) for scaffolding new projects. We're going to install the Adonis CLI, create a new project and install the projects with npm. If you do not have Node.js installed on your machine, go do that. Node.js is a prerequisite for this tutorial and can be downloaded here.

$ npm i -g adonis-cli
$ adonis new adonis-task-list
$ cd adonis-task-list
$ npm i 
$ npm run serve:dev

These commands add the adonis command to your terminal, which can generate new Adonis applications. There is a serve:dev command in the project's package.json file that starts a web server on http://localhost:3333. When we run these commands, we will see the Adonis.js welcome page at that address:

Adonis Welcome Page

The home (/) route is defined in app/Http/routes.js:

const Route = use('Route')

Route.on('/').render('welcome')  

This call renders the welcome page that is defined in resources/views/welcome.njk. By default, Adonis uses the Nunjucks library from Mozilla for HTML templating and stores the web application's views in the resources/views directory.

We encourage you to check out the official documentation on Adonis application structure. The general layout is as follows:

├── app
│   ├── Commands
│   ├── Http
│   ├── Listeners
│   ├── Model
├── bootstrap
├── config
├── database
│   ├── migrations
│   └── seeds
├── providers
├── public
├── resources
│   └── views
├── storage

We're not going to cover what every single file and folder does in this code, but at a high level this is what you need to know about the Adonis app structure:

The app folder holds our Controllers, Models, Middleware, and Routes.

The config folder, combined with the .env file, is where we define our configuration for connecting to the database and our methods of authentication.

The database holds the orders for setting up our database, called Migrations. Additionally, this folder holds instructions for seeding the database with initial data using Model Factories

The resources folder holds our Nunjucks templates and layouts for the content that renders to the browser.

Initial templating

Now that we've generated our application, we can update the welcome page template:

resources/views/welcome.njk

{% extends 'master' %}

{% block content %}
  <section class="hero">
  <div class="hero-body">
    <div class="container">
      <h1 class="title">
        Welcome to the website
      </h1>
      <h2 class="subtitle">
        A solution for creating tasks, built with Adonis.js
      </h2>
    </div>
  </div>
</section>  
{% endblock %}

On the first line of welcome.njk, we're making a call that this file extends a master file. We've made some updates to the welcome file, but we'd also like to add a navbar and the Bulma CSS library for our views. Head to the master.njk file to make some changes

resources/views/master.njk

<!doctype html>  
<html>  
<head>  
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

  <title>Task List - Adonis.js</title>

  <link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,200,300,600,700,900' rel='stylesheet' type='text/css'>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.0/css/bulma.css">
  <link rel="icon" href="/assets/favicon.png" type="image/x-icon">
  <link rel="stylesheet" type="text/css" href="style.css">
</head>  
<body>  
  <div class='container'>
    <nav class="navbar" role="navigation" aria-label="main navigation">
      <div class="navbar-brand">
        <a class='navbar-item' href='/'>Task List</a>
      </div>
      <div class='navbar-menu'>
        <div class='navbar-start'>
          <a class='navbar-item' href='/tasks'>Tasks</a>

          {% if currentUser %}
            <a class='navbar-item' href='/tasks/create'>Create task</a>
          {% endif %}
        </div>
        <div class='navbar-end'>
          {% if not currentUser %}
            <a class='navbar-item' href='/login'>Login</a>
            <a class='navbar-item' href='/register'>Register</a>
          {% else %}
            <div class="navbar-item has-dropdown is-hoverable">
              <a class="navbar-link">
                {{ currentUser.username }}
              </a>
              <div class="navbar-dropdown">
                <a class="navbar-item" href='/logout'>
                  Logout
                </a>
              </div>
            </div>
          {% endif %}
        </div>
      </div>
    </nav>

    <main>
      {% block content %}{% endblock %}
    </main>
  </div>

</body>  
</html>  

How our application works so far is that we hit the home route, which makes a call to render the welcome view. The welcome view extends the master layout so the full page renders. There is quite a bit going on in the above master file, so let's break it down:

  • {% block content %}{% endblock %}: This line denotes where views will render. Our welcome view calls the master template and renders its content in this section within the <main> HTML tags. When we route to new templates that extend the master layout, this section will be updated, though the rest of the page - such as the head and navbar - will stay the same.

  • {% if not currentUser %}: currentUser is a session based helper for checking whether there is a user logged in and who they are.

  • class="navbar-item has-dropdown is-hoverable": These are Bulma specific styles for creating a navbar with hoverable dropdown functionality. You can view the Bulma nav docs here.

Create a local database

Before we get any further, we need to set up a database for storing tasks and users. We will define a Task database model that will correspond to a MySQL table. You will need to have MySQL set up on your local machine. Full instructions for getting set up are available here.

$ mysql -uroot -p
> create database adonistasklist;

If you do not have MySQL configured on your machine, or prefer to use another database such as Postgres or SQLite, that is totally cool. All of our configuration for the database will live in our .env file. This file is in .gitignore by default, meaning it will not show up in version control. If you deploy your app to production using git, you’ll have to re-configure these values. Your production and development databases will most likely use different credentials!

Add variables to your .env file:

HOST=localhost  
PORT=3333  
APP_KEY=n96M1TPG821EdN4mMIjnGKxGytx9W2UJ  
NODE_ENV=development  
CACHE_VIEWS=false  
SESSION_DRIVER=cookie  
DB_CONNECTION=mysql  
DB_HOST=127.0.0.1  
DB_PORT=3306  
DB_USER=root  
DB_PASSWORD=XXXXXXX  
DB_DATABASE=adonistasklist  

Next up, install the MySQL npm package: npm i --save mysql

We've done some generic setup to connect our Adonis app to MySQL. Every Adonis application ships with an interactive shell command line tool called Ace. We can use Ace to generate models, migrations, controllers, and more. For a full list of the available commands, check out ./ace --help.

We're going to use Ace to generate a task model and a migration file. These commands will specify our database columns and data instance behaviors. Adonis ships with a powerful ORM called Lucid to help model our data. The advantage of this approach is that we can write Javascript that translates into database queries, instead of setting up everything using SQL.

$ ./ace make:model Task
create: app/Model/Task.js  
$ ./ace make:migration tasks --create=tasks
create: database/migrations/1509056291034_tasks.js  
$ ./ace migration:run
✔ Database migrated successfully in 737 ms

If we view the database you created with a MySQL tool such as Sequel Pro, we can see that the database includes a table called "tasks" that includes columns for id, created_at and updated_at. These fields are defined in the migration file we created. In our application, we’d like tasks to have a title and a description. Additionally, a particular task will belong to a user. So, we’ll add that field now and set up the rest of the user logic in one moment.

database/migrations/XXXXX_tasks.js

'use strict'

const Schema = use('Schema')

class TasksTableSchema extends Schema {

  up () {
    this.create('tasks', (table) => {
      table.increments()
      table.timestamps()
      table.string('user_id')
      table.string('title')
      table.text('description')
    })
  }

  down () {
    this.drop('tasks')
  }

}

module.exports = TasksTableSchema  

Migrations in Adonis use Knex.js query builder, so you can refer to that documentation.

We can also specify that a Task belongs to a user:

app/Model/Task.js

'use strict'

const Lucid = use('Lucid')

class Task extends Lucid {  
    user () {
        return this.belongsTo('App/Model/User')
    }
}

module.exports = Task  

Next, we’ll run the refresh command that will rollback our migrations and set up our database anew with the user_id, title and description columns we just added to the tasks table.

$ ./ace migration:refresh

Add authentication

Authentication is a massive topic around which whole companies are built. Adonis provides multiple modes of authentication out of the box. In this tutorial, we are going to set up session authentication through default ace commands:

$ ./ace auth:setup
create: app/Model/Token.js  
create: database/migrations/1509056843032_create_users_table.js  
create: database/migrations/1509056843033_create_tokens_table.js  
create: app/Model/User.js  

This command generates a users table and user model that we'll use as a starting point for our application. The tokens model and tokens table are used for storing the session information for a particular user. A user will remain logged in for the duration of their session and a session token will be associated with a user. Check out the files in app/Model and you'll see that a user has many Tokens and a token belongs only to a user. If you are not familiar with SQL database relationships, Ruby On Rails has an excellent guide that covers the concepts here

Seed database

Adonis has built-in capabilities for seeding data. We want to create some users and tasks so that when we fire up our server we don't see blank data. Adonis uses chance.js under the hood to mock out information. We can call chance methods in our database factories. The Adonis docs on seeds and factories are available here

First, we call our factories from the database seeder in database/seeds/Database.js:

const Factory = use('Factory')

class DatabaseSeeder {

  * run () {
    yield Factory.model('App/Model/User').create(5)
    yield Factory.model('App/Model/Task').create(5)

  }

}

module.exports = DatabaseSeeder  

If you are not familiar with the concept of database factories, they are essentially blueprints for creating instances of database records. As you can see in the code above, we call the model factories to create five users and five tasks. These model factories are defined in database/factory.js and that file can look like so:

Factory.blueprint('App/Model/User', (fake) => {  
  return {
    username: fake.username(),
    email: fake.email(),
    password: fake.password()
  }
})

Factory.blueprint('App/Model/Task', (fake) => {  
  return {
    title: fake.sentence(),
    description: fake.paragraph(),
    user_id: 1
  }
})

Here, we are using a chance.js to populate random sentences, usernames, emails, passwords, and paragraphs. These will all be unique database records that we can populate our database with. If you are brand new to the concept of database seeding and model factories, Laravel has some great documentation on the topic.

In order to create our users table and the corresponding database records, we must run the following commands.

$ ./ace migration:reset 
$ ./ace migration:run
$ ./ace db:seed

Reset will drop the tables and clear the database. The run command will create all of the tables from scratch and the db:seed command will generate the records that we defined above. The records will be populated there!

Rendering our seeded data to the screen

Define route resource

So now we've got a web server running and records in the database. Still, this doesn't do us much good unless we can render this information to a webpage. In order to do that, we're going to need to define some routes and controllers.

Adonis, similarly to other MVC frameworks, has the concept of resourceful routes. It is common to need to create, list, show, update and delete a record, and it can be cumbersome to define all of these actions separately. By embracing convention over configuration, we can define all of these actions within one line in the app/Http/routes.js file:

Route.resource('tasks', 'TaskController')  

After adding this line, run ./ace route:list from the command line and we'll see all the routes defined for our application:

┌────────┬───────────┬─────────────────┬────────────────────────┬─────────────┬───────────────┐
│ Domain │ Method    │ URI             │ Action                 │ Middlewares │ Name          │
├────────┼───────────┼─────────────────┼────────────────────────┼─────────────┼───────────────┤
│        │ GET|HEAD  │ /               │ Closure                │             │ /             │
├────────┼───────────┼─────────────────┼────────────────────────┼─────────────┼───────────────┤
│        │ GET|HEAD  │ /tasks          │ TaskController.index   │             │ tasks.index   │
├────────┼───────────┼─────────────────┼────────────────────────┼─────────────┼───────────────┤
│        │ GET|HEAD  │ /tasks/create   │ TaskController.create  │             │ tasks.create  │
├────────┼───────────┼─────────────────┼────────────────────────┼─────────────┼───────────────┤
│        │ POST      │ /tasks          │ TaskController.store   │             │ tasks.store   │
├────────┼───────────┼─────────────────┼────────────────────────┼─────────────┼───────────────┤
│        │ GET|HEAD  │ /tasks/:id      │ TaskController.show    │             │ tasks.show    │
├────────┼───────────┼─────────────────┼────────────────────────┼─────────────┼───────────────┤
│        │ GET|HEAD  │ /tasks/:id/edit │ TaskController.edit    │             │ tasks.edit    │
├────────┼───────────┼─────────────────┼────────────────────────┼─────────────┼───────────────┤
│        │ PUT|PATCH │ /tasks/:id      │ TaskController.update  │             │ tasks.update  │
├────────┼───────────┼─────────────────┼────────────────────────┼─────────────┼───────────────┤
│        │ DELETE    │ /tasks/:id      │ TaskController.destroy │             │ tasks.destroy │
└────────┴───────────┴─────────────────┴────────────────────────┴─────────────┴───────────────┘

Now that we have successfully created, seeded, and rendered our database, this seems like a good place to end our part 1.

Feel free to advance to part 2, where we will dive into defining controllers, handling authentication, and (finally) adding task views.