Build a REST API with AdonisJs and TDD Part 4
Emanuel Quimper
Posted on December 3, 2019
In this part number 4, we will continue working on our API. But now we will also make a request to another service call TheMovieDB Api.
This is finally an API where we can get info about a certain movie. In this part, we will create a new controller where the user will able to search
for a certain movie title. We first check if the movie already exists in our database. If not we then query the 3rd party API to get the info. When we got
that info we will persist them in our own database.
First, we will create a test call SearchMovie
this will be another functional one.
adonis make:test SearchMovie
The first few tests will be just about the fact those movies are already inside our
database. This will make this simpler. Later for the test, we will mock TheMovieDB
so this way we will not exceed our request quotas.
// test/functional/search-movie.spec.js
'use strict'
const Factory = use('Factory')
const { test, trait } = use('Test/Suite')('Search Movie')
trait('Test/ApiClient')
trait('Auth/Client')
test('can query for a certain movie title', async ({ assert, client }) => {
await Factory.model('App/Models/Movie').create({ title: 'Joker' })
const response = await client.get('/api/movies?title=Joker').end();
response.assertStatus(200)
response.assertJSONSubset([{
title: 'Joker',
}])
})
If you run the test you will get an error like this
can query for a certain movie title
TypeError: Cannot read property 'name' of undefined
at Factory.model
This means we didn't define yet our factory for the movie.
// database/factory.js
'use strict'
/*
|--------------------------------------------------------------------------
| Factory
|--------------------------------------------------------------------------
|
| Factories are used to define blueprints for database tables or Lucid
| models. Later you can use these blueprints to seed your database
| with dummy data.
|
*/
/** @type {import('@adonisjs/lucid/src/Factory')} */
const Factory = use('Factory')
Factory.blueprint('App/Models/User', faker => {
return {
username: faker.username(),
email: faker.email(),
password: 'password123'
}
})
Factory.blueprint('App/Models/Challenge', faker => {
return {
title: faker.sentence(),
description: faker.sentence()
}
})
Factory.blueprint('App/Models/Movie', (faker, index, data) => {
return {
title: faker.sentence(),
...data
}
})
If you check, the factory take 3 arguments and the third one is for getting data from when you call the factory. So you can overide value just like that.
If you rerun the test with npm t
you will get now a new error. This error is about the fact then we
do not have yet a model Movie
and our factory tries to create one with it. For this run the command
adonis make:model Movie -m
If you remember the -m
means give me a migration file at the same time. We will just win some time with this.
Now the test will show this
Error: SQLITE_ERROR: table movies has no column named title
Pretty self explain the error, we try to add a title to but no column yet is defined. Time to add this to the migration file we just did create.
'use strict'
/** @type {import('@adonisjs/lucid/src/Schema')} */
const Schema = use('Schema')
class MovieSchema extends Schema {
up () {
this.create('movies', (table) => {
table.increments()
table.string('title').notNullable()
table.timestamps()
})
}
down () {
this.drop('movies')
}
}
module.exports = MovieSchema
After this we get
expected 404 to equal 200
404 => 200
Normal the route is not created yet. Add this to your routes.js
file
// start/routes.js
Route.group(() => {
Route.get('/', 'MovieController.index')
}).prefix('/api/movies')
Now almost the same error but if you check carefully you will see now the error is about a 500
error not 404
like before. It's because the controller does not exist yet.
Time to make an HTTP controller
adonis make:controller Movie
Ooooh, same error? Yes, we did use a method called index
but our controller is empty.
// app/Controllers/Http/MovieController.js
'use strict'
class MovieController {
async index({}) {
}
}
module.exports = MovieController
It's time now to do some stuff to fix the new error about 204
for no-content
.
We first need to get the query title and after this fetch our database with this and return that with a 200
status code.
// app/Controllers/Http/MovieController.js
'use strict'
const Movie = use('App/Models/Movie')
class MovieController {
async index({ request, response }) {
const movies = await Movie.query()
.where('title', request.input('title'))
.fetch()
return response.ok(movies)
}
}
module.exports = MovieController
The input method in the request object gives us a way to fetch the query argument we want. In this case that was the title where we did put Joker
in it. If you run your test at this point this will work.
But... I don't like that. First, in this way of doing, we need a match of 100% the title. What happens if the user just put jok
and not the full Joker
title. Time to create a new test for this case.
test('can query with a subset of the title', async ({ assert, client }) => {
await Factory.model('App/Models/Movie').create({ title: 'Joker' })
const response = await client.get('/api/movies?title=jok').end();
response.assertStatus(200)
response.assertJSONSubset([{
title: 'Joker',
}])
})
Now when you run the test we see that fail. Time to make use of a real query search
// app/Controllers/Http/MovieController.js
'use strict'
const Movie = use('App/Models/Movie')
class MovieController {
async index({ request, response }) {
const title = request.input('title')
const movies = await Movie.query()
.where('title', 'LIKE', `%${title}%`)
.fetch()
return response.ok(movies)
}
}
module.exports = MovieController
Now this works with this change. This will make sure if a subset of the title is present at least we still give the movie to the user.
Time to force the user to provide a title pretty simple one here too
test('should throw 400 if no title is pass', async ({ assert, client }) => {
const response = await client.get('/api/movies').end()
response.assertStatus(400)
})
// app/Controllers/Http/MovieController.js
'use strict'
const Movie = use('App/Models/Movie')
class MovieController {
async index({ request, response }) {
const title = request.input('title')
if (!title) {
return response.status(400).json({ error: 'title is required' })
}
const movies = await Movie.query()
.where('title', 'LIKE', `%${title}%`)
.fetch()
return response.ok(movies)
}
}
module.exports = MovieController
Conclusion
In the next part, we will jump on the TheMovieDB
API stuff. We will learn how we can mock an external API so it's easier to test.
I hope you enjoy the post. Don't hesitate to comment below.
Source Code: https://github.com/EQuimper/adonis-tdd-tutorial-demo/tree/part-4
Happy Coding :)
This is a cross-platform post from my blog. You can read the original here: https://equimper.com/blog/build-a-rest-api-with-adonisjs-and-tdd-part-4
Posted on December 3, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.