DEV Community

Adam Miedema
Adam Miedema

Posted on • Updated on

Creating a REST API using Adonis v5

In this multi-part tutorial, we'll create and deploy a multi-server architecture web application.

Part 1: Creating a front-end using NuxtJS and TailwindCSS
Part 2: Setting up Adonis v5 with PostgreSQL
Part 3: Creating a REST API using Adonis v5
Part 4: Connecting Nuxt front-end to Adonis 5 API
Part 5: Deploying a multi-server app with Cleavr

Frameworks and tools used

Part 3 - Creating a REST API using Adonis v5 and PostgreSQL

Let's dig a little deeper into Adonis and create a Rest API. For this, we'll need to create a data model, routes, and controller. We'll also set up a database table migration and create a seeder to seed the database with.

Set up RESTful API routes

Adonis makes the standard CRUD operations easily available for you so that you don't have to define everything yourself.

Using Route.resource() will automatically set up all of the typical CRUD routes for you. Let's add in the following route:

Route
  .resource('movies', 'MoviesController')
  .only(['index', 'show'])
  .apiOnly()

What this does is set up our CRUD routes for /movies and assigns them to refer to the MoviesController, in which we'll define later on.

However, since I am making a simple app and am only interested in fetching either all of the movies or a single movie, I'll use .only() to only include the index and show routes.

We'll also add .apiOnly(). Otherwise, .resource() will create form routes; which, we don't need.

Run the following command in your terminal to verify that only the index and show routes are created.

node ace list:routes

Make a data model class

To create a new model, use the following command:

node ace make:model Movie

This operation creates a new file at /app/Models/Movie.ts and gives us a head start with creating the model.

Recalling the data points that we need for our front-end, we'll define the model class for Movies as follows:

import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'

export default class Movie extends BaseModel {
  @column({ isPrimary: true })
  public id: number

  @column()
  public title: String

  @column()
  public posterImage: String

  @column()
  public releaseYear: Number

  @column()
  public topBilled: String

  @column()
  public genres: String

  @column()
  public movieDescription: String

  @column()
  public movieReview: String

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime
}

Make a data migration and migrate the database

Run the following command to create a new migration:

node ace make:migration movies

This will create a file located at /database/migrations/<timestamp>_movies.ts.

Let's navigate to the migration file and add in the table columns.

import BaseSchema from '@ioc:Adonis/Lucid/Schema'

export default class Movies extends BaseSchema {
  protected tableName = 'movies'

  public async up () {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.timestamps(true)
      table.string('title').notNullable()
      table.string('poster_image').notNullable()
      table.string('release_year').notNullable()
      table.string('top_billed').notNullable()
      table.string('genres').notNullable()
      table.string('movie_description').notNullable()
      table.string('movie_review').notNullable()
    })
  }

  public async down () {
    this.schema.dropTable(this.tableName)
  }
}

Time to migrate the database by running the following command:

node ace migration:run

Let's double check our database to see if the table migrated successfully.

I like to use TablePlus to manage my databases; so, I'll open that up to check that a migration row was added to the adonis_schema table and that my movies table and it's columns were properly migrated. And... They look good! 🎉

Make a seeder and seed the database

We'll now set up a seeder to seed our database with the example movies that we identified while creating the front-end - Children of Men, The Terminator, and The Sisterhood of the Traveling Pants.

node ace make:seeder Movies

The above command will output a new seeder file at databases/seeders/Movies.ts.

Let's import the Movie data model and then add JSON for the 3 sets of movie details we want to seed our database with.

import BaseSeeder from '@ioc:Adonis/Lucid/Seeder'
import Movie from "App/Models/Movie";

export default class MovieSeeder extends BaseSeeder {
  public async run () {
    await Movie.createMany([
      {
        title: 'Children of Men',
        posterImage: 'https://m.media-amazon.com/images/M/MV5BMTQ5NTI2NTI4NF5BMl5BanBnXkFtZTcwNjk2NDA2OQ@@._V1_.jpg',
        releaseYear: 2006,
        topBilled: 'Clive Owen, Julianne Moore, Michael Cane',
        genres: 'Adventure, Drama, Sci-Fi',
        movieDescription: 'In 2027, in a chaotic world in which women have become somehow infertile, a former activist agrees to help transport a miraculously pregnant woman to a sanctuary at sea.',
        movieReview: 'The movie is pretty good. It\'s known for long, seemingly un-cut sequences. It\'s better than Gravity. It\'s not as good as Galaxy Quest. You should watch it. I mean, what else are you doing?'
      },
      {
        title: 'The Terminator',
        posterImage: 'https://m.media-amazon.com/images/M/MV5BYTViNzMxZjEtZGEwNy00MDNiLWIzNGQtZDY2MjQ1OWViZjFmXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_SY1000_CR0,0,666,1000_AL_.jpg',
        releaseYear: 1984,
        topBilled: 'Arnold Schwarzenegger, Michael Biehn, Linda Hamilton',
        genres: 'Action, Sci-Fi',
        movieDescription: 'A human soldier is sent from 2029 to 1984 to stop an almost indestructible cyborg killing machine, sent from the same year, which has been programmed to execute a young woman whose unborn son is the key to humanity\'s future salvation.',
        movieReview: 'Like most movies that turn into a series, the 2nd one is wayyyyy better. In this movie, Schwarzenegger is the bad guy. The great thing for him, is that he\'s not required to act and just plays his normal self. Hollywood. A wonderland!'
      },
      {
        title: 'The Sisterhood of the Traveling Pants',
        posterImage: 'https://m.media-amazon.com/images/M/MV5BNmRjYWE3OTQtYzEwOC00OWM4LTk3MzktZTUyZTgzNjY4NDc0L2ltYWdlL2ltYWdlXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_UX182_CR0,0,182,268_AL_.jpg',
        releaseYear: 2005,
        topBilled: 'Amber Tamblyn, Alexis Bledel, America Ferrera, Blake Lively',
        genres: 'Comedy, Drama, Romance',
        movieDescription: 'Four best girlfriends hatch a plan to stay connected with one another as their lives start off in different directions: they pass around a pair of secondhand jeans that fits each of their bodies perfectly.',
        movieReview: 'This movie is about the greatest miracle the world has seen. The pants fit them all! Furthermore, the pants travel all over the world. They\'re like a traveling gnome. But, more fun to watch!'
      },
    ])
  }
}

Now, to seed the database, run:

node ace db:seed

At this step, I was asked to install luxon prior to being able to successfully run the seed command. You can install this package running the following:

npm install --save luxon

Once you have successfully ran the seed command, go back to your database management client and verify that the data was correctly seeded.

Make a controller class

Time to create the controller class!

node ace make:controller MoviesController

This will create a controller file at app/Controllers/Http/MoviesController.ts.

Head over to the new file and we'll start by adding in the Movie model.

import Movie from "App/Models/Movie"

Next, we'll add in our index function.

export default class MoviesController {

  public async index () {
    const movies = await Movie.all()
    return movies
  }
}

The .all() method is a shorthand to query and return everything from the movies table.

We'll use this route for our homepage that displays the list of movies.

Now, for our details page, we only want to return data for a single movie.

Let's add in the following to our MovieController class:

public async show ({ params }) {
    const movie = await Movie.find(params.id)
    return movie
  }

We're passing in params.id in the .find function to pull the movie id from the request url and then looking up the movie in our database using the returned id.

We now have all routes and controller defined. Time to test the API! 😎

Test the API

To test the API, I'll use Postman; though, you can use your favorite web browser if you do not want to use an API client.

In Postman, I'll make a Get request and add in my local address and append the url with /movies.

This request will trigger the index router and then return the full list of movies as we defined in MoviesController.

CleanShot 2020-09-20 at 11.11.10@2x

It works!

Now, let's check passing a movie id to verify it only returns data for that movie.

CleanShot 2020-09-20 at 11.11.32@2x

Success! 🔥

We now have our API ready and will update the front-end to dynamically call and place the data from the API in part 4.


Following along? View Part 2 progress on GitHub at https://github.com/armgitaar/moviesapi.

Watch the full tutorial series uninterrupted on Youtube.

Top comments (0)