With over 8,000 stars on GitHub and being one of the top Node.js web frameworks on the Node.js website, Adonis.js is on its way to being a major player in the Node.js community as a dependable framework for building web applications. Its sleek and concise API makes it very developer-friendly, easily scalable and highly performant. In this tutorial, we will build a simple API with Adonis.js and automate its deployment to Heroku.

Prerequisites

To follow this tutorial, a few things are required:

  1. Basic knowledge of JavaScript
  2. Node.js installed on your system (>= 8.0)
  3. Adonis.js CLI installed globally (Run npm i -g @adonisjs/cli to install)
  4. An Heroku account
  5. A CircleCI account
  6. A GitHub account

With all these installed and set up, let’s begin the tutorial.

Creating an Adonis.js API project

First, create a new, API-only Adonis.js app by running the command below:

adonis new my-adonis-api --api-only

This will scaffold an Adonis.js project that is only structured for building API endpoints, not web pages. The project will be placed in the my-adonis-api folder specified after the adonis new command. Once the scaffolding process is done, go into the root of the project (cd my-adonis-api) and run the following command to boot up the application:

adonis serve --dev

This will launch a local server at http://localhost:3333, which you can access via your browser or by using curl on your CLI. Hitting this root endpoint will return the JSON data below:

{ "greeting": "Hello world in JSON" }

Setting up SQLite for local development

Before we start writing our API code, we need a local development database to work with. By default, our Adonis.js project comes configured to use a SQLite database with settings to connect to the database found in ./config/database.js.


module.exports = {

  connection: Env.get("DB_CONNECTION", "sqlite"),


  sqlite: {
    client: "sqlite3",
    connection: {
      filename: Helpers.databasePath(
        `${Env.get("DB_DATABASE", "development")}.sqlite`
      ),
    },
    useNullAsDefault: true,
    debug: Env.get("DB_DEBUG", false),
  },

  .....
}

And in the environment configuration file at the root of the project (.env), we also have some configuration for the database to be used.

DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_DATABASE=adonis
HASH_DRIVER=bcrypt

This configuration defines the database connection to use (DB_CONNECTION) and database name (DB_DATABASE). The connection here is set to sqlite, and the database name is set to adonis. That part is good to go.

Now that we have our database in place, the next step is to install the sqlite3 Node.js package. This is the driver for connecting to our SQLite database. Install the package by running the following command:

npm install sqlite3 --save

With this package installed, next we will run the migrations necessary to set up our database schema. Adonis.js uses migrations to set up and update the database schema programmatically in order to easily replicate the schema on different environments and maintain consistency across development teams and deployment environments.

Migration files can be found in ./database/migrations. By default, this folder contains two migrations, one for a users table and another for a tokens table. To run these migrations and set up our database schema, run the following command at the root of the project:

adonis migration:run

The command above will create a new adonis.sqlite database file in the ./database folder (as it doesn’t exist yet) and run our migrations.

Migrations Run

We now have everything set up to work with our SQLite database. Next, we will write code for a simple User API that allows us to create and fetch user records.

Creating the API’s user model

To begin developing our User API, we need to define our User model. Adonis.js models can be found in the ./app/Models folder. Locate the User.js file in this directory (this file is created during the scaffolding process), and enter the following code:

"use strict";

const Model = use("Model");

const Hash = use("Hash");

class User extends Model {
  static get table() {
    return "users";
  }

  static boot() {
    super.boot();

    this.addHook("beforeSave", async (userInstance) => {
      if (userInstance.dirty.password) {
        userInstance.password = await Hash.make(userInstance.password);
      }
    });
  }

  tokens() {
    return this.hasMany("App/Models/Token");
  }

  static async createUser(data) {
    const newUser = await this.create(data);

    return newUser;
  }

  static async getUsers(filter) {
    const allUsers = await this.query().where(filter).fetch();

    return allUsers;
  }
}

module.exports = User;

In the code above, we create a User model that extends the base Model class. We then define a table getter to return the name of the table this model references. Next, we add a beforeSave hook that ensures that our plain text passwords are encrypted before they are saved to the database upon user creation.

A tokens relationship function is created to reference the Token model, which also exists in the /app/Models directory. This relationship allows us to fetch each user’s access token upon login.

Finally, we create two more model functions, one to create a new user (createUser) and the other to fetch a list of users based on a query filter (getUsers).

Creating the API user controller

Our next task is to create a User controller. Adonis.js is a model view controller (MVC) framework, so we need controllers to handle our API requests.

We are going to do some validation within our controller methods. That means that we need to set up our project with the Adonis.js validation package. To implement validation in Adonis.js, install the validator package with the following command:

adonis install @adonisjs/validator

Once the package is installed, add the following item to the providers array in ./start/app.js:


const providers = [
    ....,
    '@adonisjs/validator/providers/ValidatorProvider'
]

Create a new controller by running the following command:

adonis make:controller User --type http

This command will create a new folder named Http in the ./app/Controllers and create a new file UserController.js within this folder. Inside the newly created controller, replace the code in the file with the code below:

"use strict";

const Logger = use("Logger");
const { validate } = use("Validator");
const User = use("App/Models/User");

class UserController {
  async create({ request, response }) {
    const data = request.post();

    const rules = {
      username: `required|unique:${User.table}`,
      email: `required|unique:${User.table}`,
      password: `required`
    };

    const messages = {
      "username.required": "A username is required",
      "username.unique": "This username is taken. Try another.",
      "email.required": "An Email is required",
      "email.unique": "Email already exists",
      "password.required": "A password for the user"
    };

    const validation = await validate(data, rules, messages);

    if (validation.fails()) {
      const validation_messages = validation.messages().map((msgObject) => {
        return msgObject.message;
      });

      return response.status(400).send({
        success: false,
        message: validation_messages
      });
    }

    try {
      let create_user = await User.createUser(data);

      let return_body = {
        success: true,
        details: create_user,
        message: "User Successully created"
      };

      response.send(return_body);
    } catch (error) {
      Logger.error("Error : ", error);
      return response.status(500).send({
        success: false,
        message: error.toString()
      });
    }
  } //create

  async fetch({ request, response }) {
    const data = request.all();

    try {
      const users = await User.getUsers(data);

      response.send(users);
    } catch (error) {
      Logger.error("Error : ", error);
      return response.status(500).send({
        success: false,
        message: error.toString()
      });
    }
  } //fetch
}

module.exports = UserController;

In the code above, we create two controller functions (create and fetch), which create a new user and fetch a list of users, respectively. In the create function, we use our Validator module to validate the request data to ensure that every compulsory item for creating a new user is present. We also set up appropriate error messages for our validation.

Registering routes on the API

It is now time for us to register our routes as the final step to developing our API. Open the file ./start/routes.js and replace the code in it with the following:

"use strict";

const Route = use("Route");

Route.get("/", () => {
  return { greeting: "Welcome to the Adonis API tutorial" };
});

//User routes
Route.group(() => {
  Route.post("create", "UserController.create");

  Route.route("get", "UserController.fetch", ["GET", "POST"]);
}).prefix("user");

In the above file, we change the default greeting message in the / route to Welcome to the Adonis API tutorial. Next, we register the /create and /get routes that map to the create and fetch functions of the UserController, respectively. We prefix these two endpoints with /user to add some route namespacing.

Testing the endpoints in Postman

Let’s now put our API to the test by calling our endpoints. We will use Postman to test our endpoints. Make sure that your app is running. If it is not, run adonis serve --dev to start it up again.

Below are tests for user creation and fetching users:

User Creation - Validation failed

User Creation - Validation Failed

User Creation - Successful

User Creation - Successful

User Fetch

User Fetch

Setting up Heroku and MySQL for production deployment

Everything we have done so far works perfectly on our local machine, but the aim of this tutorial is to get our API hosted on a production environment and automating the deployment process. Thus, instead of running our API on our machine, we will be hosting it on the Heroku platform. Also, instead of using SQLite, we will be using MySQL, which is a more robust relational database management system appropriate for production environments.

We also want to make sure that when we are running on our local machine, SQLite is used, and when running in production, our code automatically switches to MySQL.

To host our API on Heroku, we need to create a Heroku app. Log into your Heroku account and create a new application.

Heroku App

Next, we need to create a remote MySQL instance. Lucky for us, we have the ability to access add-ons on Heroku. One of those add-ons is a MySQL instance via ClearDB.

Note: To have add-ons on your Heroku applications, you need to set up billing on Heroku. Make sure to add a billable card on your account settings.

To add a MySQL add-on, go to the Resources tab of your application and search for MySQL as shown below.

Add MySQL

Select the ClearDB option to set up the MySQL instance. On the add-on pop up screen, choose the free Ignite plan.

Add MySQL

Click Provision to set up the database. Once this is done, it is added to your list of add-ons and a new CLEARDB_DATABASE_URL environment variable will be added to your application. It can be found in the Config Vars section of your application’s Settings page.

On that page, click Reveal Config Vars to reveal your environment variables. Now, add two other environment variables to this list:

  • APP_KEY: Your application’s API key found in your .env file
  • DB_CONNECTION: To ensure that MySQL is used in production and not SQlite, set this to mysql

The final step in setting up our production database is to configure our mysql connection in ./config/database.js. We need the url-parse package to help us correctly resolve the connection string to our MySQL database on ClearDB. We also need the mysql package as our driver to connect to our production database. Install these packages with the following command:

npm install url-parse mysql --save

Now, replace everything in ./config/database.js with the code below:

"use strict";

const Env = use("Env");

const Helpers = use("Helpers");
const URL = require("url-parse");
const PROD_MYSQL_DB = new URL(Env.get("CLEARDB_DATABASE_URL"));

module.exports = {
  connection: Env.get("DB_CONNECTION", "sqlite"),

  sqlite: {
    client: "sqlite3",
    connection: {
      filename: Helpers.databasePath(
        `${Env.get("DB_DATABASE", "adonis")}.sqlite`
      )
    },
    useNullAsDefault: true,
    debug: Env.get("DB_DEBUG", false)
  },

  mysql: {
    client: "mysql",
    connection: {
      host: Env.get("DB_HOST", PROD_MYSQL_DB.host),
      port: Env.get("DB_PORT", ""),
      user: Env.get("DB_USER", PROD_MYSQL_DB.username),
      password: Env.get("DB_PASSWORD", PROD_MYSQL_DB.password),
      database: Env.get("DB_DATABASE", PROD_MYSQL_DB.pathname.substr(1))
    },
    debug: Env.get("DB_DEBUG", false)
  }
};

In the above file, we have configured our mysql connection to make use of our ClearDB instance in production, and the sqlite connection will be used as a fallback on our local machine.

Configuring the project on CircleCI

Our next task is to get our project set up on CircleCI. First, you need to initialize your Adonis.js project app as a git repository (git init) and push your code to a GitHub repository on the account that is linked to your CircleCI account.

Next, go to the Add Projects page on the CircleCI dashboard.

Add Project - CircleCI

Click Set Up Project to begin. This will load the next screen.

Add Config - CircleCI

On the setup page, click Add Manually to instruct CircleCI that we will be adding a configuration file manually and not using the sample displayed. Next, you get a prompt to either download a configuration file for the pipeline or start building.

Build Prompt - CircleCI

Click Start Building to begin the build. This build will fail because we have not set up our configuration file yet. We will do this later on.

The final thing we need to do on the CircleCI console is to set up environment variables for the project we just added. This will enable it to have authenticated access to our Heroku application for deployments.

Go to your project’s settings by clicking Project Settings on the Pipelines page (make sure your project is the currently selected project).

Project settings - CircleCI

On this page, click Environment Variables on the side menu.

Environment variables - CircleCI

Click Add Environment Variable to add a new environment variable.

Add Environment variable - CircleCI

Add the following environment variables:

  • HEROKU_APP_NAME: This is the name of your Heroku application (in this case my-adonis-api-app)
  • HEROKU_API_KEY: Your Heroku account API key found under the Account tab of your Heroku account under Account Settings.

Once added, you now have everything set up on your CircleCI console for deployment to Heroku.

Automating the deployment of the API

We are now at the final task of automating the deployment of our Adonis.js API to the Heroku hosting platform. In my opinion, this is the simplest step.

We need to create a Heroku Procfile to provide instructions for Heroku about how we want our application to be deployed. At the root of the project, create a file named Procfile (no file extension). Paste the following commands into it:

release: ENV_SILENT=true node ace migration:run --force
web: ENV_SILENT=true npm start

In the file above, we are instructing Heroku to run our migrations using node ace migration:run --force. This is an alternative to the adonis migration:run command. This is used intentionally because the Heroku environment does not have the Adonis CLI installed globally as we have on our local machine. This step is done in Heroku’s release phase.

Next, we instruct Heroku to run our application using the npm start command.

We have prefixed both commands with ENV_SILENT=true to suppress some Adonis.js warnings as it tries to look for a .env file whose purpose has been replaced with the environment variables we set earlier on our Heroku application.

Now we can write our deployment script. At the root of your the project, create a folder named .circleci and a file within it named config.yml. Inside the config.yml file, enter the following code:

version: 2.1
orbs:
  heroku: circleci/heroku@0.0.10
workflows:
  heroku_deploy:
    jobs:
      - heroku/deploy-via-git

In the configuration above, we pull in the Heroku orb (circleci/heroku@0.0.10), which automatically gives us access to a powerful set of Heroku jobs and commands. One of those jobs is heroku/deploy-via-git, which deploys your application straight from your GitHub repo to your Heroku account. This job already takes care of installing the Heroku CLI, installing project dependencies, and deploying the application. It also picks up our CircleCI environment variables to facilitate a smooth deployment to our Heroku application.

Commit all the changes made to our project and push to your repo to trigger the deployment. If all instructions have been followed, you will have built a successful deployment pipeline.

Build Successful - CircleCI

To see the behind-the-scenes action of the deployment, click build.

Build Details - CircleCI

If you look closely, the screen above shows that our migrations ran successfully. For full confirmation that the deployment of our application has been successful, visit the default Heroku address for the site https://[APP_NAME].herokuapp.com. In our case, this is https://my-adonis-api-app.herokuapp.com/.

API Live - Heroku

Note: I am using a JSON formatter on my browser, so your display format may be different.

Now you can run some tests from Postman on your production API.

API Live - Heroku

API Live - Heroku

Awesome!

As seen from the screens above, we now have a working production API deployed to Heroku.

Conclusion

Building APIs is fun with Adonis.js. In this tutorial, we learned how to create an automated continuous deployment (CD) pipeline using CircleCI to automatically deploy our Adonis.js API to Heroku every time we push code to our repository. We also learned to configure different database systems for our development and production environments.

Happy coding!


Fikayo is a fullstack developer and author with over a decade of experience developing web and mobile solutions. He is currently the Software Lead at Tech Specialist Consulting and develops courses for Packt and Udemy. He has a strong passion for teaching and hopes to become a full-time author.