Node.js movie app - Complete the application

We want to see the details of a selected movie but so far we only have a list of movies. We will need to write another prompt and make another request to fetch the details we want. Movie app with Vorpal.js, part 4.

Part 1: Create the framework

Part 2: Make it interactive

Part 3: Add real data

We’ve made a good progress on the movie app and as a result real data are fetched from the database. In this post we will use the response to select one movie and finally display the details.

Complete the project

Currently the movies are listed in the following format:

{
  "Search": [
    {
      "Title": "Taken",
      "Year": "2008",
      "imdbID": "tt0936501",
      "Type": "movie",
      "Poster": "https://m.media-amazon.com/images/M/MV5BMTM4NzQ0OTYyOF5BMl5BanBnXkFtZTcwMDkyNjQyMg@@._V1_SX300.jpg"
    },
    {
      "Title": "Taken 2",
      "Year": "2012",
      "imdbID": "tt1397280",
      "Type": "movie",
      "Poster": "https://m.media-amazon.com/images/M/MV5BMTkwNTQ0ODExOV5BMl5BanBnXkFtZTcwNjU3NDQwOA@@._V1_SX300.jpg"
    },
    {
      "Title": "Taken 3",
      "Year": "2014",
      "imdbID": "tt2446042",
      "Type": "movie",
      "Poster": "https://m.media-amazon.com/images/M/MV5BNjM5MDU3NTY0M15BMl5BanBnXkFtZTgwOTk2ODU2MzE@._V1_SX300.jpg"
    },
// ...
  ],
  "totalResults": "187",
  "Response": "True"
}

We see from the Response property that our request was successful and we also received a bunch of movies, although these data only contain a few properties. We want to select one of the movies and write its details to the console.

Create a list of movies

The API will return the data of a movie if we provide it with the imdbID property. This unique id will identify the movie and we’ll be able to fetch the long-awaited details with the help of it.

The flow. But the user won’t be able to match the id to the movie, so we’ll need to give them a list of titles (and the year the movie was made) so that they can be aware of their selection. When the user makes the choice, we’ll submit the id to the database and request the details of the related movie.

In order to do that we’ll create another prompt that offers a list of movie titles and years and then saves the user’s choice.

Map through the list. We’ll need the Title, Year and imdbId properties from the Search object.

As we saw earlier Inquirer.js has a list input type, where we need to provide an array of options and the module will write these options to the console so that the user can select one of them.

Because we need an array, we can use the map method. We’ll iterate over the Search array, select the necessary properties from each object and return an array. We can then make the value of the choice property in the new prompt equal to that array.

Create the function. Let’s create then a new function called createMovieTitleList in the user-inputs.js file, which returns the array with the options:

const createMovieTitleList = (movies) => {
  return movies.map(({ Title: title, Year: year, imdbID: id }) => {
    return {
      name: `${ title } (${ year })`,
      value: id
    };
  });
};

The function has a movies argument, which is the Search array. We iterate over this array and extract the properties we need (Title, Year and imdbId) using object destructuring.

To avoid working with capital letters I save them as title, year and id, respectively.

For each movie object in the Search array we’ll return another object, where the name property will be the title of the movie and the year in parentheses and the value property will contain the id of the movie.

Create the new prompt. We can now create a new prompt using the array returned by createMovieTitleList. We can do this in the same file (user-inputs.js):

const getSelectedMovieFromUser = (movies) => {
  if (movies.Error) {
    return Promise.reject('The requested movie is not found.');
  }

  return inquirer.prompt([
    {
      type: 'list',
      name: 'movieId',
      message: 'Select a movie from the list',
      choices: createMovieTitleList(movies.Search)
    }
  ]);
};

First we check if there’s any error with the request to the API. If so, the response object will have an Error property with a string message Movie not found!. So there’s an error, we’ll return a custom error message wrapped in a Promise.

Return a promise. Why is it needed? The prompt method returns a Promise, but when an error occurs, we want to display a custom error message and as such, prompt will never be called. This way getSelectedMovieFromUser would return a promise one time and a string at another.

We don’t want that. getSelectedMovieFromUser should always return a Promise, so when we write our custom error message, we wrap it into a Promise, and because it’s an error message, we reject the promise. The rejected promise will be caught in the catch block in index.js.

Add the choices. All we have left is to add the array returned by createMovieTitleList to the choices property and add the Search object as a parameter.

Export both functions. Both functions will be needed elsewhere, so we’ll need to export them:

module.exports = {
  getMovieTitleFromUser,
  getSelectedMovieFromUser,
  createMovieTitleList
};

We’ll use getSelectedMovieFromUser in index.js and will write tests for createMovieTitleList later as well.

Select a movie

Now that we have a list of movies based on the first prompt, let’s select the one we want to receive more information about.

Add it to Vorpal. Import getSelectedMovieFromUser to index.js:

const { getMovieTitleFromUser, getSelectedMovieFromUser } = require('./modules/user-inputs');

All we have to do now is to call this function with the movieDataFromDB object we received as a result of the first request to the API inside the try block:

// ...
const movieDataFromDB = await getMovieDataFromDB(movieTitle, movieType, apiKey);
const { movieId } = await getSelectedMovieFromUser(movieDataFromDB);
vorpal.log(chalk.green(JSON.stringify(movieId, null, 2)));
// ...

As we need the id for the next request to fetch the details, we’ll save it in a movieId variable (the same name we gave it inside the prompt). For now let’s just log the id to the console to ensure that we are doing well.

If you run the script and enter a title you should see the list of titles with the years in parentheses. Select one of them and you will see the id in the console.

Get the details

We have the id and now we can make another request to the API to fetch the details.

Create the request. Let’s create another request function in requests.js, which will return the details of the selected movie:

const getMovieDetails = (movieId, apiKey) => {
  const options = {
    method: 'GET',
    uri: `https://www.omdbapi.com/?i=${ movieId }&apikey=${ apiKey }`,
    json: true
  };

  return rp(options);
}

This request is similar to the first one; the only difference is that we use the movieId to fetch the details of the movie.

We have to export this function as well:

module.exports = { getMovieDataFromDB, getMovieDetails };

Display the details. Let’s head over to index.js and import getMovieDetails:

const { getMovieDataFromDB, getMovieDetails } = require('./modules/requests');

Again, object destructuring makes it possible to write the required functions in just one line.

We can now make the request and save the response in a temporary details variable and log it to the console. Inside the try block, we can write the following:

// ...
const { movieId } = await getSelectedMovieFromUser(movieDataFromDB);
const details = await getMovieDetails(movieId, apiKey);
vorpal.log(chalk.green(JSON.stringify(details, null, 2)));
// ...

Let’s run the script again and you’ll be amazed by the abundance of data on your favourite movie!

Select properties. This section is optional and depends on personal preferences. I don’t want to see every property which has been returned because I’m only interested in the most important data, like title, actors or plot.

Using object destructuring we can select the properties we want and display only those instead of details:

const {
  Title: title,
  Year: year,
  Runtime: runtime,
  Director: director,
  Actors: actors,
  Plot: plot,
  imdbRating: rating
} = await getMovieDetails(movieId, apiKey);
vorpal.log(chalk.green(
  JSON.stringify({ title, year, runtime, director, actors, plot, rating }, null, 2)
));

This is it! We will now see these properties of the selected movie upon running the script. Try it!

Refactor code

As we want to test the functions of the application, I would slightly refactor the code and save the validation of the first prompt in its own function.

In user-inputs.js create a function called isTitleEntered and copy the value of the validate property to the body of this function and change validate accordingly:

// inside getMovieTitleFromUser()
// ...
validate: isTitleEntered
// ...

const isTitleEntered = (title) => {
  if (title) return true;
  return 'Please enter a title!';
};

Let’s export it:

module.exports = {
  getMovieTitleFromUser,
  getSelectedMovieFromUser,
  createMovieTitleList,
  isTitleEntered
};

We can now write a unit test to this function as well and this will be the topic of the next post.

The full code is available on Github. Feel free to change it and play around with it.

Conclusion

The app is now complete and fully functional! If you followed along in the last few weeks, congratulations and I’m happy to have had you here!

Next time we’ll write tests to the application using Mocha, Chai and Sinon.

Thanks for reading and see you then.