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
- 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" }
- 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
- 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.
- 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).
- 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.
- 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
- 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.
To host our API on Heroku, we need to create a Heroku app. Log into your Heroku account and create a new application.
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.
- 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.
- 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
- 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.
Click Set Up Project to begin. This will load the next screen.
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.
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).
- 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.
- 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.
- 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.
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
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
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
- 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.
- 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
- 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",
},
]);
});
- Run the test with this command.
Run :
adonis test
Hopefully, the test passes.
How everything works in a nutshell.
Very first we register the Test/ApiClient trait, which gives us an HTTP client to make requests on a URL.
We create a dummy user before hitting the user/get URL.
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