How to Create A Simple API with AdonisJs Deploy and Test

Requirement: Basic Understanding of the following: NodeJs, Git, Heroku. Have an IDE like VS Code installed. Have Postman or any api testing platform.

How to Setup AdonisJs for Deployment and Testing

Prerequisites To follow this tutorial, a few things are required:

Basic knowledge of JavaScript Node.js installed on your system (>= 8.0) Adonis.js CLI installed globally (Run npm i -g @adonisjs/cli to install) An Heroku account A CircleCI account A GitHub account With all these installed and set up, let’s begin the tutorial.

 ## A. Setting up AdonisJs
  1. To install Run:
    npm i -g @adonisjs/cli

Creating an Adonis.js API project Create a new, API-only Adonis.js app by running the command below:

Run: 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:

Run: cd my-adonis-api

To boot up the application:

Run: adonis serve --dev

This will launch a local server at 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" }

  1. 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.

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

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:

Run: npm install sqlite3 --save

  1. 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:

Run: 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.

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.

  1. 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:

//User.js code start

"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;

    //User.js code end

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).

  1. 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: Run:

adonis install @adonisjs/validator

NOTE: 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:

Run:

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:

// UserController.js code start

    "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 Successfully 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;

// UserController.js code end

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.

  1. 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:

// route.js code start

"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");

    // route.js code end

In the above file, we change the default greeting message in the / route to Welcome to the Adonis API tutorial.

Then, 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. We can name our prefix as we like.

The endpoint will now be (localhost:3333/user/create) or (localhost:3333/user/get)

To see list of available api endpoints we can run the following command.

Run:

adonis route:list

NOTE: To see list of helpful commands

Run :

adonis --help

You will be presented with the following in the console.

Usage:
  command [arguments] [options]

Global Options:
  --env                Set NODE_ENV before running the commands  --no-ansi            Disable colored output
  --version            output the version number

Available Commands:
  addon                Create a new AdonisJs addon
  install              Install Adonisjs provider from npm/yarn 
and run post install instructions
  new                  Create a new AdonisJs application       
  repl                 Start a new repl session
  seed                 Seed database using seed files
  serve                Start Http server
  test                 Run application tests
 key
  key:generate         Generate secret key for the app
 make
  make:command         Make a new ace command
  make:controller      Make a new HTTP or Websocket channel controller
  make:ehandler        Make a new global exception handler     
  make:exception       Make a new exception
  make:hook            Make a new lucid model hook
  make:listener        Make a new event or redis listener      
  make:middleware      Make a new HTTP or Ws Middleware        
  make:migration       Create a new migration file
  make:model           Make a new lucid model
  make:provider        Make a new provider
  make:seed            Create a database seeder
  make:test            Create sample test file
  make:trait           Make a new lucid trait
  make:validator       Make route validator
  make:view            Make a view file
 migration
  migration:refresh    Refresh migrations by performing rollback and then running from start
  migration:reset      Rollback migration to the first batch   
  migration:rollback   Rollback migration to latest batch or to a specific batch number
  migration:run        Run all pending migrations
  migration:status     Check migrations current status
 route
  route:list           List all registered routes
 run
  run:instructions     Run instructions for a given module
  1. 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
    ## B. Deployment


### Setting up Heroku and MySQL for production deployment

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.

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

  2. 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.

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

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.
  1. 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:

Run:

npm install url-parse mysql --save
  1. Now, replace everything in ./config/database.js with the code below:
// database.js code start

"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)
  }
};

    // database.js code end

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.

C. Setting up for CICD

A. Configuring the project on CircleCI

Our next task is to get our project set up on CircleCI.

1.Push your project to GitHub.

2.Go to the Add Projects page on the CircleCI dashboard. Visit CircleCI homepage.

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

  2. 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.

  3. 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.

  4. 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).

  1. Click Add Environment Variable to add a new environment variable. 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.

## B. 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.

  1. 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:
// code start

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

// code end

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.

  1. 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:
// config.yml code start
version: 2.1
orbs:
  heroku: circleci/heroku@0.0.10
workflows:
  heroku_deploy:
    jobs:
      - heroku/deploy-via-git

    // config.yml code end

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.

  1. 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. NOTE: To see the behind-the-scenes action of the deployment, click build. fig. 13 fig. 14

  2. 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 my-adonis-apis-app.herokuapp.com. fig. 15

  3. Now you can run some tests from Postman on your production API. We now have a working production API deployed to Heroku.

    D. Setting up and Running Test

    Part 1: Introduction to Testing and Setup:

Every time you make a change to your application, you want to test the behavior to ensure that it works fine. Manually testing changes by visiting each web page or API is impossible and hence automated testing makes sure that everything works fine.

In this guide, we learn about the benefits and different ways to test your application.

### Test cases

The habit of writing tests, drastically improve our code quality and confidence.

To build a better mental model, testing is divided into multiple categories, so that you can write different types of test cases with a clear boundary.

### A. Unit tests

Unit tests are written to test small pieces of code in isolation. For example: Testing a service directly, without worrying about how that service is used in real world.

Unit tests ensure that each part of the application works fine on its own and also it is easier to write them since you do not need the whole application to work before you can test it.

### B. Functional tests

Functional tests are written to test your app like an end user. Which means opening up a browser programmatically and visiting the web pages to ensure they all work fine.

## Setup

1.Set up the testing engine by installing it from npm.

Run:

adonis install @adonisjs/vow
  1. Make sure to register the provider inside aceProviders array, since we do not want to boot testing engine when running your application in production.
// start/app.js code start

const aceProviders = [
  '@adonisjs/vow/providers/VowProvider'
]

    // start/app.js code end

NOTE: Once @adonisjs/vow is installed, it creates a sample test for you, along with some other files described below.

vowfile.js: The vowfiles.js is loaded before your tests are executed. You can use this file to define tasks that should occur before and after running all the tests.

.env.testing : This file contains the environment variables to be used when running tests. This file gets merged with .env file so must only define values you want to override from the .env file.

test : All of the application tests are stored inside subfolders of test directory.

### Running tests

Vow provider automatically creates a unit test for you, which can be executed by running the following command.

Run:

adonis test

Output

Example
   make sure 2 + 2 is 4 (2ms)

PASSED
total       : 1
passed      : 1
time        : 6ms

Testing suite & traits

Before we dive into writing tests, let’s understand some fundamentals which are important to understanding the flow of tests.

Suite

Each file is a test suite, which defines a group of tests of same behavior. For example, We can have a suite of tests for User registration.

const Suite = use('Test/Suite')('User registeration')

// or destructuring
const { test } = use('Test/Suite')('User registeration')
The test function obtained from the Suite instance is used for defining tests.

test('return error when credentials are wrong', async (ctx) => {
  // implementation
})

Traits

Traits are building blocks for your test suite. Since AdonisJs test runner is not bloated with a bunch of functionality, we ship different pieces of code as traits.

For example: Using the browser to run your test.

const { test, trait } = use('Test/Suite')('User registeration')

trait('Test/Browser')

test('return error when credentials are wrong', async ({ browser }) => {
  const page = await browser.visit('/user')
})

 OR 
trait('Test/AppClient')

test('return error when credentials are wrong', async ({ client}) => {
  const page = await client.visit('/user')
})

The beauty of this approach is that Traits can enhance your tests transparently without doing much work. For instance, if we remove Test/Browser or Test/AppClient trait. The browser/client object gets undefined inside our tests.

Also, you can define your traits either by defining a closure or an IoC container binding.

You do not have to create a trait for everything. Majority of the work can be done by using Lifecycle hooks.

Traits are helpful when you want to bundle a package to be used by others.

// code start

const { test, trait } = use('Test/Suite')('User registeration')

trait(function (suite) {
  suite.Context.getter('foo', () => {
    return 'bar'
  })
})

test('foo must be bar', async ({ foo, assert }) => {
  assert.equal(foo, 'bar')
})

    // code end

Context

Since each test has an isolated context, you can pass values to it by defining getters or macros and access them inside the test closure.

By default, the context has only one property called assert, which is an instance of chaijs/assert to run assertions.

Lifecycle hooks

Each suite has some lifecycle hooks, which can be used to perform repetitive tasks, like cleaning the database after each test and so on.

// code start
const Suite = use('Test/Suite')('User registeration')

const { before, beforeEach, after, afterEach } = Suite

before(async () => {
  // executed before all the tests for a given suite
})

beforeEach(async () => {
  // executed before each test inside a given suite
})

after(async () => {
  // executed after all the tests for a given suite
})

afterEach(async () => {
  // executed after each test inside a given suite
})

    // code end
###  Assertions

The assert object is an instance of chaijs/assert which is passed to each test as a property on test context.

To make your tests more reliable, you can also plan assertions to be executed for a given test. Let’s consider this example.

// code start

test('must throw exception', async ({ assert }) => {
  try {
    await badOperation()
  } catch ({ message }) {
    assert.equal(message, 'Some error message')
  }
})

    // code end

The above test passes even if an exception was never thrown and no assertions were run. Which means it is a bad test, which is passed because we structured it badly.

To overcome this situation, you must plan some assertions, to make sure the catch block is always executed and an assertion has been made.

test('must throw exception', async ({ assert }) => {
  assert.plan(1)

  try {
    await badOperation()
  } catch ({ message }) {
    assert.equal(message, 'Some error message')
  }
})

This time, if badOperation doesn’t throw an exception, the test still fails since we planned for 1 assertion and 0 were made.

Part 2: REST API tests

In this guide, we learn how to write tests against an API server.

Basic example

Let’s get started with a basic example to test an endpoint that returns a list of users in JSON.

Upcoming example assumes that you have database tables set up with a User model.

  1. Let’s start by creating a functional test since we are testing the API endpoint like an end-user.

Run:

adonis make:test User

Output

create: test/functional/user.spec.js

  1. Let’s open the test file and paste following code inside it.
"use strict";

const { test, trait } = use("Test/Suite")("User");
const User = use("App/Models/User");

trait("Test/ApiClient");

test("create a new user", async ({ client }) => {
  // create a user data
  let data = {
    username: "user1",
    email: "testing1@gmail.com",
    password: "testing1234",
  };

  await User.create({
    username: data.username,
    email: data.email,
    password: data.password,
  });
  // always put .end() to your method to initiate it.
  const response = await client.get("/user/get").end();
  console.log("Browser response: ", response.text);

  response.assertStatus(200);
  // check if the response contain the data
  // even when we are not sure of if location
  response.assertJSONSubset([
    {
      username: data.username,
      email: data.email,
    },
  ]);
});

test("get the home page", async ({ client }) => {
  const response = await client.get("/").end();
  console.log(response.text);
  response.assertStatus(200);
  // check if the text is equal to the response
  response.assertText(
    "Welcome to the Adonis API tutorial ,updated welcome page!"
  );
});

test("get a specific user by id", async ({ client }) => {
  const response = await client.get("/user/get/?id=1").end();
  console.log(response.text);

  response.assertStatus(200);
  response.assertJSONSubset([
    {
      username: "user1",
      email: "testing1@gmail.com",
    },
  ]);
});
  1. Run the test with this command.

Run :

adonis test

Hopefully, the test passes.

How everything works in a nutshell.

  1. Very first we register the Test/ApiClient trait, which gives us an HTTP client to make requests on a URL.

  2. We create a dummy user before hitting the user/get URL.

  3. Finally, we run assertions to make sure the return HTTP status is 200 with one user having the same username and email.

NOTE: The end method ends the HTTP request chain and returns the response. Make sure always to call the 'end' method.

const response = await client.get("/user/get/?id=1").end();

Congratulations 👏 You have got your first test passing.

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. We also learn how to setup and run test on our application.

References: Setup:

1. https://circleci.com/blog/automating-the-deploy-of-an-adonis-api-to-heroku/
 2. https://github.com/adonisjs-community/awesome-adonisjs
3. https://gist.github.com/alaomichael/2a1edacfdd00f0d69693ed62e0a1e251
4. https://github.com/alaomichael/adonis-commands
5.https://my-adonis-apis-app.herokuapp.com/

Testing :

1. https://legacy.adonisjs.com/docs/4.0/testing
2. https://legacy.adonisjs.com/docs/4.0/api-tests