Node.js movie app - Complete the application
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.