Build REST APIs in Adonis 4.0

build-rest-apis-in-adonis-4-0-header.png

In this tutorial, we are focused to build a RESTful app using Adonis 4.0, an open source Node.js web framework focused on simplicity and ease of use.

Introduction

In this tutorial, we are using Adonis 4.0 to build a RESTful application. Adonis is an open source Node.js web framework with primary focus on simplicity and ease of use. It was inspired by Laravel and borrows a lot of features from the PHP framework. The framework comes with features such as Job queues, an authentication system, integration with email systems, data validation, and supports Lucid ORM. The framework is created and maintained by Aman Virk

Requirements

The following tools are used in this article:
Adonis 4.0
Git
MySQL
Postman
Node (A basic knowledge of Node and ES6/7)

You can go ahead and install Node, Git, and Postman. We will walk through installing Adnois and MySQL in the tutorial.

What are RESTful APIs?

A REST (REpresentational State Transfer) web service is one way of providing communication between applications and services on the Internet. An API (Application Programming Interface) is a set of protocols or tools used in building an application. RESTful APIs allows the system to communicate via HTTP using HTTP verbs like GET, POST, PUT and DELETE. GET is used to retrieve resources, POST is used to store new resources, PUT is used to update existing resources and DELETE is deleting an existing resource. For this tutorial, our resources will be Books.

Let’s create a new application. Open your terminal or command prompt and type the following command:

1# if you don't have Adonis CLI installed on your machine. 
2    $ npm i -g @adonisjs/cli
3
4    # Create a new Adonis app
5    $ adonis new adonisjs-restful-api --api-only

Make sure you have Git installed for the adonis command to work

After that is done, navigate into application’s directory and start your server to test if all is working:

1$ cd adonisjs-restful-api
2
3    $ adonis serve --dev

Open Postman and make a request to http://127.0.0.1:3333. You should see this sample JSON.

Migrations and Models

Before we start with the migrations, make sure you have your database set-up for this application and add the credentials to the .env file in the project’s root directory.

1DB_CONNECTION=mysql
2    DB_HOST=127.0.0.1
3    DB_PORT=3306
4    DB_DATABASE=adonis-restful
5    DB_USERNAME=root
6    DB_PASSWORD=adonis

Let’s get started with our first migration and model – Book. A book should have a Title, an ISBN, an Author, a Publisher and a Creation Date. We will be using the Adonis CLI to create the files and put them in the appropriate folder. To create the Book model, we can run:

1adonis make:model Book --migration

The --``migration “ argument tells adonis CLI to create a migration file along with the model file.

Go to the database/migrations directory and modify the migration file as follow:

1// <timestamp>_book_schema.js
2
3    'use strict'
4
5    const Schema = use('Schema')
6
7    class BookSchema extends Schema {
8      up () {
9        this.create('books', (table) => {
10          table.increments()
11          table.string('title').nullable()
12          table.string('isbn').nullable()
13          table.string('publisher_name').nullable()
14          table.string('author_name').nullable()
15          table.timestamps()
16        })
17      }
18
19      down () {
20        this.drop('books')
21      }
22    }
23
24    module.exports = BookSchema

up() runs when we migrate while down() runs when we rollback.

Before you migrate the table, let’s install the MySql module:

1$ npm install --save mysql

Now, we can go ahead and migrate:

1$ adonis migration:run

Next, go to the model file called Book.js in app/Models and add references to the table and primary key:

1'use strict'
2
3    const Model = use('Model')
4
5    class Book extends Model {
6      static get table () {
7        return 'books'
8      }
9
10      static get primaryKey () {
11        return 'id'
12      }
13    }
14
15    module.exports = Book

Routes and controllers

Let’s work on the endpoints of our application: create, fetch the list, fetch a single resource, update and delete. In our start/routes.js file, we will just do this:

1'use strict'
2
3    const Route = use('Route')
4    const Book = use('App/Models/Book')
5
6    Route.group(() => {
7      Route.post('books', async ({ request, response }) => {
8        const bookInfo = request.only(['title', 'isbn', 'publisher_name', 'author_name'])
9
10        const book = new Book()
11        book.title = bookInfo.title
12        book.isbn = bookInfo.isbn
13        book.publisher_name = bookInfo.publisher_name
14        book.author_name = bookInfo.author_name
15
16        await book.save()
17
18        return response.status(201).json(book)
19      })
20
21      Route.get('books', async ({ response }) => {
22        let books = await Book.all()
23
24        return response.json(books)
25      })
26
27      Route.get('books/:id', async ({ params, response }) => {
28        const book = await Book.find(params.id)
29
30        return response.json(book)
31      })
32
33      Route.put('books/:id', async ({ params, request, response }) => {
34        const bookInfo = request.only(['title', 'isbn', 'publisher_name', 'author_name'])
35
36        const book = await Book.find(params.id)
37        book.title = bookInfo.title
38        book.isbn = bookInfo.isbn
39        book.publisher_name = bookInfo.publisher_name
40        book.author_name = bookInfo.author_name
41
42        await book.save()
43
44        return response.status(200).json(book)
45      })
46
47      Route.delete('books/:id', async ({ params, response }) => {
48        const book = await Book.find(params.id)
49        if (!book) {
50          return response.status(404).json(null)
51        }
52        await book.delete()
53
54        return response.status(204).json(null)
55      })
56    }).prefix('api/v1')

The routes created in the start/routes.js will be prefixed with api/v1. If you noticed from the code, we grouped all our routes because they share the prefix api/v1.

When your application grows pretty large, you might want to move this entire logic to a different controller class. You can use the CLI tools to make controllers:

1adonis make:controller BookController

The CLI tool will ask you what the controller is for. Select Http Request

Check your app/Controllers/Http``/ directory and directory where you will discover a controller called BookController``.js has been created.

Replace BookController.js with the logic originally in your route file:

1'use strict'
2
3    const Book = use('App/Models/Book')
4    class BookController {
5      async index ({response}) {
6        let books = await Book.all()
7
8        return response.json(books)
9      }
10
11      async show ({params, response}) {
12        const book = await Book.find(params.id)
13
14        return response.json(book)
15      }
16
17      async store ({request, response}) {
18        const bookInfo = request.only(['title', 'isbn', 'publisher_name', 'author_name'])
19
20        const book = new Book()
21        book.title = bookInfo.title
22        book.isbn = bookInfo.isbn
23        book.publisher_name = bookInfo.publisher_name
24        book.author_name = bookInfo.author_name
25
26        await book.save()
27
28        return response.status(201).json(book)
29      }
30
31      async update ({params, request, response}) {
32        const bookInfo = request.only(['title', 'isbn', 'publisher_name', 'author_name'])
33
34        const book = await Book.find(params.id)
35        if (!book) {
36          return response.status(404).json({data: 'Resource not found'})
37        }
38        book.title = bookInfo.title
39        book.isbn = bookInfo.isbn
40        book.publisher_name = bookInfo.publisher_name
41        book.author_name = bookInfo.author_name
42
43        await book.save()
44
45        return response.status(200).json(book)
46      }
47
48      async delete ({params, response}) {
49        const book = await Book.find(params.id)
50        if (!book) {
51          return response.status(404).json({data: 'Resource not found'})
52        }
53        await book.delete()
54
55        return response.status(204).json(null)
56      }
57    }
58
59    module.exports = BookController

Then you can leave your route as thin as possible:

1const Route = use('Route')
2
3    Route.group(() => {
4      Route.post('books', 'BookController.store')
5      Route.get('books', 'BookController.index')
6      Route.get('books/:id', 'BookController.show')
7      Route.put('books/:id', 'BookController.update')
8      Route.delete('books/:id', 'BookController.delete')
9    }).prefix('api/v1')

Let’s head back to Postman again and start testing out these new routes and the controller functionalities:

  • The store method in BookController is responsible for handling HTTP POST requests to api/v1/books, and handles creation.
  • The index method in BookController is responsible for handling HTTP GET requests to api/v1/books, and handles retrieving all the book resources available.
  • The show method in BookController is responsible for handling HTTP GET requests to api/v1/books, and handles retrieving of a particular book resource.
  • The update method in BookController is responsible for handling HTTP PUT requests to api/v1/books, and handles updating of a particular book resource. It will only update it if the resource is available, otherwise, it will return an HTTP 404 Not Found error.
  • Lastly, the delete method in BookController is responsible for handling HTTP DELETE requests to api/v1/books and handles deleting of a particular book resource. If the resource is available it will delete it and return an HTTP 204 No Content, otherwise, it will return an HTTP 404 Not Found.

Please find the Github repository here.

Conclusion

Adonis is an awesome Node.js framework that brings joy and stability over anything else. I hope building this RESTful API opens your eyes to Adonis.

If you have any questions or observations, feel free to drop them in the comments section below. I would be happy to respond to you.