Plz yurn on JavaScript

Build a REST API with AdonisJs and TDD Part 4

November 21, 20192 minutes to read

  • #tutorial
  • #adonisjs
  • #tdd
  • #javascript
  • #testing

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.js
2
3'use strict'
4
5const Factory = use('Factory')
6const { test, trait } = use('Test/Suite')('Search Movie')
7
8trait('Test/ApiClient')
9trait('Auth/Client')
10
11test('can query for a certain movie title', async ({ assert, client }) => {
12 await Factory.model('App/Models/Movie').create({ title: 'Joker' })
13
14 const response = await client.get('/api/movies?title=Joker').end();
15
16 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 title
2 TypeError: Cannot read property 'name' of undefined
3 at Factory.model

This means we didn't define yet our factory for the movie.

1// database/factory.js
2
3'use strict'
4
5/*
6|--------------------------------------------------------------------------
7| Factory
8|--------------------------------------------------------------------------
9|
10| Factories are used to define blueprints for database tables or Lucid
11| models. Later you can use these blueprints to seed your database
12| with dummy data.
13|
14*/
15
16/** @type {import('@adonisjs/lucid/src/Factory')} */
17const Factory = use('Factory')
18
19Factory.blueprint('App/Models/User', faker => {
20 return {
21 username: faker.username(),
22 email: faker.email(),
23 password: 'password123'
24 }
25})
26
27Factory.blueprint('App/Models/Challenge', faker => {
28 return {
29 title: faker.sentence(),
30 description: faker.sentence()
31 }
32})
33
34Factory.blueprint('App/Models/Movie', (faker, index, data) => {
35 return {
36 title: faker.sentence(),
37 ...data
38 }
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'
2
3/** @type {import('@adonisjs/lucid/src/Schema')} */
4const Schema = use('Schema')
5
6class MovieSchema extends Schema {
7 up () {
8 this.create('movies', (table) => {
9 table.increments()
10
11 table.string('title').notNullable()
12
13 table.timestamps()
14 })
15 }
16
17 down () {
18 this.drop('movies')
19 }
20}
21
22module.exports = MovieSchema

After this we get

1expected 404 to equal 200
2404 => 200

Normal the route is not created yet. Add this to your routes.js file

1// start/routes.js
2
3Route.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.js
2
3'use strict'
4
5class MovieController {
6 async index({}) {
7
8 }
9}
10
11module.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.js
2
3'use strict'
4
5const Movie = use('App/Models/Movie')
6
7class MovieController {
8 async index({ request, response }) {
9 const movies = await Movie.query()
10 .where('title', request.input('title'))
11 .fetch()
12
13 return response.ok(movies)
14 }
15}
16
17module.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' })
3
4 const response = await client.get('/api/movies?title=jok').end();
5
6 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.js
2
3'use strict'
4
5const Movie = use('App/Models/Movie')
6
7class MovieController {
8 async index({ request, response }) {
9 const title = request.input('title')
10
11 const movies = await Movie.query()
12 .where('title', 'LIKE', `%${title}%`)
13 .fetch()
14
15 return response.ok(movies)
16 }
17}
18
19module.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()
3
4 response.assertStatus(400)
5})
1// app/Controllers/Http/MovieController.js
2
3'use strict'
4
5const Movie = use('App/Models/Movie')
6
7class MovieController {
8 async index({ request, response }) {
9 const title = request.input('title')
10
11 if (!title) {
12 return response.status(400).json({ error: 'title is required' })
13 }
14
15 const movies = await Movie.query()
16 .where('title', 'LIKE', `%${title}%`)
17 .fetch()
18
19 return response.ok(movies)
20 }
21}
22
23module.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.

Next

Golang Rest API for NodeJS developer - Part 2

Join my Newsletter

Subscribe to get my latest content & deal for my incoming courses.

    I respect your privacy. Unsubscribe at any time.

    Powered By ConvertKit

    Comments