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:
- 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.
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.
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 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;
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 - Successful
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.
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.
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
fileDB_CONNECTION
: To ensure thatMySQL
is used in production and notSQlite
, set this tomysql
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. Begin by pushing your project to GitHub.
Next, go to the Add Projects page on the CircleCI dashboard.
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).
On this page, click Environment Variables on the side menu.
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 casemy-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.
To see the behind-the-scenes action of the deployment, click build.
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/
.
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.
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 Adepoju is a LinkedIn Learning (Lynda.com) Author, Full-stack developer, technical writer, and tech content creator proficient in Web and Mobile technologies and DevOps with over 10 years experience developing scalable distributed applications. With over 40 articles written for CircleCI, Twilio, Auth0, and The New Stack blogs, and also on his personal Medium page, he loves to share his knowledge to as many developers as would benefit from it. You can also check out his video courses on Udemy.