Automating deployment of an AdonisJS API to Heroku
Fullstack Developer and Tech Author
With over 14,000 stars on GitHub, Adonis.js is one of the top Node.js web frameworks on the Node.js website. Adonis.js’ reputation as a dependable framework for building web applications has made it a major player in the Node.js community. 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 with continuous deployment (CD).
Prerequisites
To follow this tutorial, a few things are required:
- Basic knowledge of JavaScript
- Node.js installed on your system (>= 14.0)
- An Heroku account
- A CircleCI account
- A GitHub account
Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.
With all these installed and set up, let’s begin the tutorial.
Creating an Adonis.js API project
The API project will be built with AdonisJS 5. To begin, run this command:
npm init adonis-ts-app@latest my-adonis-api-heroku
You will be prompted to Select the project structure. Choose api and accept the default options for others.
You should get output like this:
_ _ _ _ ____
/ \ __| | ___ _ __ (_)___ | / ___|
/ _ \ / _` |/ _ \| '_ \| / __|_ | \___ \
/ ___ \ (_| | (_) | | | | \__ \ |_| |___) |
/_/ \_\__,_|\___/|_| |_|_|___/\___/|____/
CUSTOMIZE PROJECT
❯ Select the project structure · api
❯ Enter the project name · my-adonis-api-heroku
❯ Setup eslint? (y/N) · false
RUNNING TASKS
❯ Scaffold project 26 ms
❯ Install dependencies 37 s
❯ Configure installed packages 14 s
[ success ] Project created successfully
╭─────────────────────────────────────────────────╮
│ Run following commands to get started │
│─────────────────────────────────────────────────│
│ │
│ ❯ cd my-adonis-api-heroku │
│ ❯ node ace serve --watch │
│ │
╰─────────────────────────────────────────────────╯
This scaffolds an Adonis.js project that is structured only for building API endpoints, not web pages. The project will be placed in my-adonis-api-heroku. Once the scaffolding process is done, go into the root of the project (cd my-adonis-api-heroku ) and run:
node ace serve --watch
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 this JSON data:
{ "hello": "world" }
Setting up database for local development
Before you start writing API code, you will need a local development database to work with. You will use AdonisJS Lucid, an SQL ORM that supports PostgreSQL, MySQL, MSSQL, SQLite and others.
To install Lucid, stop the running app with CTRL + C. Run this command:
npm install @adonisjs/lucid
Once the installation is done, configure the database by running:
node ace invoke @adonisjs/lucid
Choose SQLite from the list of options and select In the Terminal for the instructions. Adonis will configure the database and store its configuration inside the config.database.ts file.
...
sqlite: {
client: 'sqlite',
connection: {
filename: Application.tmpPath('db.sqlite3'),
},
pool: {
afterCreate: (conn, cb) => {
conn.run('PRAGMA foreign_keys=true', cb)
}
},
migrations: {
naturalSort: true,
},
useNullAsDefault: true,
healthCheck: false,
debug: false,
},
In the environment configuration file at the root of the project (.env), there is more configuration for the database to use.
PORT=3333
HOST=127.0.0.1
NODE_ENV=development
APP_KEY=PZcwnLRwPn5GUMhXe-Nc94CUPnNlKEI7
DRIVE_DISK=local
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 assigns the 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.
Next, run the migrations needed to set up our database schema. Adonis.js uses migrations to set up and update the database schema programmatically to easily replicate the schema on different environments. Migrations maintain consistency across development teams and deployment environments.
node ace make:migration users
This command creates a new file called database/migrations/xxxx_users.ts. Open the file and paste this content into it:
import BaseSchema from "@ioc:Adonis/Lucid/Schema";
export default class extends BaseSchema {
protected tableName = "users";
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments("id");
table.string("username", 80).notNullable().unique();
table.string("email", 254).notNullable().unique();
table.string("password", 60).notNullable();
/**
* Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL
*/
table.timestamp("created_at", { useTz: true });
table.timestamp("updated_at", { useTz: true });
});
}
public async down() {
this.schema.dropTable(this.tableName);
}
}
Run the migration and create the database table using:
node ace migration:run
You will get output like this:
[ info ] Upgrading migrations version from "1" to "2"
❯ migrated database/migrations/1696658232595_users
Migrated in 85 ms
Everything is set up to work with your SQLite database. Next, you will write code for a simple User API for creating and fetching user records.
Creating the API’s user model
To begin developing the User API, you need to define the User model. To do so, run:
node ace make:model User
Open the app/Models/User.ts file and enter:
import { DateTime } from "luxon";
import { BaseModel, column, beforeSave } from "@ioc:Adonis/Lucid/Orm";
import Hash from "@ioc:Adonis/Core/Hash";
export default class User extends BaseModel {
@column({ isPrimary: true })
public id: number;
@column()
public username: string;
@column()
public email: string;
@column({ serializeAs: null })
public password: string;
@column.dateTime({ autoCreate: true })
public createdAt: DateTime;
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime;
@beforeSave()
public static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await Hash.make(user.password);
}
}
}
This code creates a User model. The model extends the BaseModel class with the appropriate column using the columns decorator and specified datatypes for each. The is adds a hashPassword hook that ensures that plain text passwords are encrypted before they are saved to the database upon user creation.
Creating the API user controller
Your next task is to create a User controller. Adonis.js is a model view controller (MVC) framework, so you need controllers to handle API requests.
You are going to do some validation within the controller methods. Adonisjs supports parsing and validation out of the box, so there is no need to install any 3rd party packages.
Create a new controller by running this command:
node ace make:controller User
This command creates a new folder named Http in ./app/Controllers and then creates a new file UsersController.ts within it. Inside the newly created controller, replace the code in the file with this code:
import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext";
import User from "App/Models/User";
import { schema, rules } from "@ioc:Adonis/Core/Validator";
export default class UsersController {
protected tableName = "users";
public async create({ request, response }: HttpContextContract) {
const newUserSchema = schema.create({
username: schema.string([
rules.unique({
column: "username",
table: this.tableName,
}),
rules.required(),
]),
email: schema.string([
rules.unique({
column: "email",
table: this.tableName,
}),
rules.required(),
]),
password: schema.string([rules.required()]),
});
try {
const payload = await request.validate({
schema: newUserSchema,
});
const user = new User();
user.username = payload.username;
user.email = payload.email;
user.password = payload.password;
let create_user = await user.save();
let return_body = {
success: true,
details: create_user,
message: "User Successully created",
};
response.send(return_body);
} catch (error) {
console.log(error.toString());
return response.status(500).send({
success: false,
message: error.messages,
});
}
} //create
public async fetch({ request, response }: HttpContextContract) {
try {
const users = await User.query();
response.send(users);
} catch (error) {
return response.status(500).send({
success: false,
message: error.toString(),
});
}
} //fetch
}
This code creates two controller functions (create and fetch) that create a new user and fetch a list of users. The create function creates a newSchema() method to validate the request data to ensure that every compulsory item for creating a new user is present. The code also sets up error messages for the validation.
Registering routes on the API
Now you can register your routes as the final step to developing the API. Open the file ./start/routes.js and replace the code in it with this:
import Route from "@ioc:Adonis/Core/Route";
Route.get("/", async () => {
return { hello: "world" };
});
//User api routes
Route.group(() => {
Route.post("user", "UsersController.create");
Route.get("users", "UsersController.fetch");
}).prefix("api");
This code registers the /user and /users routes that map to the create and fetch functions of the UserController. It prefixes these two endpoints with /api to add some route namespacing.
Testing the endpoints in Postman
Now you can put the API to the test by calling your endpoints. You will use Postman to test the endpoints. First, install the Bcrypt password hashing algorithm by running:
npm install --save phc-bcrypt
Make sure that your app is running. If it is not, run node ace serve --watch to start it up again.
These are the tests for user creation and fetching users:
- User Creation - Validation failed
- User Creation - Successful
- User Fetch
User Creation - Validation failed

User Creation - Successful

User Fetch

Setting up Heroku and MySQL for production deployment
Everything you have done so far works perfectly on your local machine, but the aim of this tutorial is getting your API hosted on a production environment and automating the deployment process. So, instead of running your API on your machine, you will host it on the Heroku platform. Also, instead of using SQLite, you will use MySQL, which is a more robust relational database management system that works well for production environments.
You also want to make sure that when you are running on your local machine, SQLite is used, and when running in production, your code automatically switches to MySQL.
To host the API on Heroku, you need to create a Heroku app. Log into your Heroku account and create a new application.

Next, create a remote MySQL instance. Luckily, you have the ability to access add-ons on Heroku. One of those add-ons is a MySQL instance via jawsDB.
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 jawsDB option to set up the MySQL instance. On the add-on pop-up, choose the free kitefin shared plan.

Click Provision to set up the database. Once this is done, it is added to your list of add-ons and a new JAWSDB_URL environment variable will be added to your application. You can find it 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.envfileDB_CONNECTION: To ensure thatMySQLis used in production and notSQlite, set this tomysqlHOST: set this value as0.0.0.0DRIVE_DISK: This configures the file system. set its value aslocal
The final step in setting up the production database is to configure the mysql connection in ./config/database.js. The url-parse package helps you correctly resolve the connection string to the MySQL database on jawsDB. You also need the mysql package as a driver to connect to the production database. Install these packages with this command:
npm install url-parse mysql --save
Now, replace everything in ./config/database.js with this code:
"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.hostname),
port: Env.get("DB_PORT", PROD_MYSQL_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),
},
};
This file configures your mysql connection to make use of the jawsDB instance in production. The sqlite connection will be used as a fallback on your local machine.
Automating the deployment of the API
Now you can start automating the deployment of your Adonis.js API to the Heroku hosting platform. In my opinion, this is the simplest step.
Create a Heroku Procfile to provide instructions for Heroku about how you want your application to be deployed. At the root of the project, create a file named Procfile (no file extension). Paste these commands into it:
release: ENV_SILENT=true node ./build/ace migration:run --force
web: ENV_SILENT=true node ./build/server.js
This file instructs Heroku to run your migrations using node ./build/ace migration:run --force. This step is done in Heroku’s release phase.
Next, instruct Heroku to run your application.
You have prefixed both commands with ENV_SILENT=true. The prefix suppresses 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 the Heroku application.
Adding CircleCI configuration
Now you can write the deployment script. At the root of the project, create a folder named .circleci and a file within it named config.yml. Inside the config.yml file, enter this code:
version: 2.1
jobs:
deploy:
docker:
- image: cimg/base:2023.09
steps:
- checkout
- run:
name: Deploy app to Heroku
command: |
git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git main
workflows:
build-deploy:
jobs:
- deploy
This configuration uses Git to deploy your latest code changes to your Heroku account.
Get your project set up on CircleCI by pushing your project to GitHub.
Log into your CircleCI account. If you signed up with your GitHub account, all your repositories will be available on your project’s dashboard.
Click Set Up Project next to your my-adonis-api-heroku project.
CircleCI detects the config.yml file for the project. Click Use Existing Config and then Start Building. Your first workflow will start running, but it will fail.

The deployment process fails because you have not provided the Heroku API key. To fix that, click the Project Settings button, then click Environment Variables. Add two new variables.
- For
HEROKU_APP_NAME, add the app name you used in Heroku. The name will be eithermy-adonis-api-herokuor a custom name (if you created one). - For
HEROKU_API_KEYenter the Heroku API key that you retrieved earlier from the Account Settings page.
Re-run your workflow from the start, and this time your workflow will run successfully.

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

Confirm that your workflow was successful by opening your newly deployed app in a browser. The URL for your application should be in this format: https://<HEROKU_APP_NAME>-<RANDOM_NUMBER>.herokuapp.com/. You can find the generated domain name for your app on the Settings page.

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


Awesome!
You now have a working production API deployed to Heroku.
Conclusion
Building APIs is fun with Adonis.js. In this tutorial, you learned how to create an automated CD pipeline using CircleCI. The pipeline automatically deploys your Adonis.js API to Heroku every time you push code to your repository. You also learned to configure different database systems for development and production environments.
The complete source code can be found here on Github.
Happy coding!