Build a REST API with AdonisJs and TDD Part 4
November 21, 2019 • 2 minutes to read
Intro
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.
1adonis 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.
1// test/functional/search-movie.spec.js23'use strict'45const Factory = use('Factory')6const { test, trait } = use('Test/Suite')('Search Movie')78trait('Test/ApiClient')9trait('Auth/Client')1011test('can query for a certain movie title', async ({ assert, client }) => {12 await Factory.model('App/Models/Movie').create({ title: 'Joker' })1314 const response = await client.get('/api/movies?title=Joker').end();1516 response.assertStatus(200)17 response.assertJSONSubset([{18 title: 'Joker',19 }])20})
If you run the test you will get an error like this
1can query for a certain movie title2 TypeError: Cannot read property 'name' of undefined3 at Factory.model
This means we didn't define yet our factory for the movie.
1// database/factory.js23'use strict'45/*6|--------------------------------------------------------------------------7| Factory8|--------------------------------------------------------------------------9|10| Factories are used to define blueprints for database tables or Lucid11| models. Later you can use these blueprints to seed your database12| with dummy data.13|14*/1516/** @type {import('@adonisjs/lucid/src/Factory')} */17const Factory = use('Factory')1819Factory.blueprint('App/Models/User', faker => {20 return {21 username: faker.username(),22 email: faker.email(),23 password: 'password123'24 }25})2627Factory.blueprint('App/Models/Challenge', faker => {28 return {29 title: faker.sentence(),30 description: faker.sentence()31 }32})3334Factory.blueprint('App/Models/Movie', (faker, index, data) => {35 return {36 title: faker.sentence(),37 ...data38 }39})
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
1adonis 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
1Error: 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.
1'use strict'23/** @type {import('@adonisjs/lucid/src/Schema')} */4const Schema = use('Schema')56class MovieSchema extends Schema {7 up () {8 this.create('movies', (table) => {9 table.increments()1011 table.string('title').notNullable()1213 table.timestamps()14 })15 }1617 down () {18 this.drop('movies')19 }20}2122module.exports = MovieSchema
After this we get
1expected 404 to equal 2002404 => 200
Normal the route is not created yet. Add this to your routes.js
file
1// start/routes.js23Route.group(() => {4 Route.get('/', 'MovieController.index')5}).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
1adonis make:controller Movie
Ooooh, same error? Yes, we did use a method called index
but our controller is empty.
1// app/Controllers/Http/MovieController.js23'use strict'45class MovieController {6 async index({}) {78 }9}1011module.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.
1// app/Controllers/Http/MovieController.js23'use strict'45const Movie = use('App/Models/Movie')67class MovieController {8 async index({ request, response }) {9 const movies = await Movie.query()10 .where('title', request.input('title'))11 .fetch()1213 return response.ok(movies)14 }15}1617module.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.
1test('can query with a subset of the title', async ({ assert, client }) => {2 await Factory.model('App/Models/Movie').create({ title: 'Joker' })34 const response = await client.get('/api/movies?title=jok').end();56 response.assertStatus(200)7 response.assertJSONSubset([{8 title: 'Joker',9 }])10})
Now when you run the test we see that fail. Time to make use of a real query search
1// app/Controllers/Http/MovieController.js23'use strict'45const Movie = use('App/Models/Movie')67class MovieController {8 async index({ request, response }) {9 const title = request.input('title')1011 const movies = await Movie.query()12 .where('title', 'LIKE', `%${title}%`)13 .fetch()1415 return response.ok(movies)16 }17}1819module.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
1test('should throw 400 if no title is pass', async ({ assert, client }) => {2 const response = await client.get('/api/movies').end()34 response.assertStatus(400)5})
1// app/Controllers/Http/MovieController.js23'use strict'45const Movie = use('App/Models/Movie')67class MovieController {8 async index({ request, response }) {9 const title = request.input('title')1011 if (!title) {12 return response.status(400).json({ error: 'title is required' })13 }1415 const movies = await Movie.query()16 .where('title', 'LIKE', `%${title}%`)17 .fetch()1819 return response.ok(movies)20 }21}2223module.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.