Node.js movie app - Add Inquirer.js

In this part of the creating a CLI movie app series we will make the application interactive. By the end of the post it will be able to request input from the user and write the response to the console. Part 2.

Part 1: Movie app with Vorpal.js - Create the framework

In Part 1 we created the framework for our CLI application by installing figlet, Chalk and Vorpal. But our app still contains dummy data, so it’s time to make it interactive.

Inquirer.js

Inquirer.js is a command line interface, which makes the application interactive by advanced error handling, validation and input parsing.

Creating the first prompt

Let’s go to the project folder again and install Inquirer.js by typing npm install --save inquirer. Once it’s done, we need to require it at the top of index.js:

const inquirer = require('inquirer');

We are now ready to write the first prompt.

Prompts. We need to start somewhere and if the user wants to search for a movie, the title of the movie (or some part of it when they are not sure about it) will help to get detailed information about it.

But what is a prompt?

Inquirer.js comes with a prompt method, where we can define our message (or question) to the user, the input type and we can also provide some validation to make sure the user doesn’t do anything funky.

These are all parts of a question object which is placed inside an array, and that array will be the argument of prompt.

The prompt will return a Promise.

Promises can easily be handled with the async/await syntax. The function which will contain the prompt (inside vorpal.action) is already marked with the async keyword, if not, we can do it now.

Writing the prompt. We can now write the first prompt inside vorpal.action. Delete the line that logs the “Coming soon!” message and paste the following code instead:

try {
  const movieTitleByUser = await inquirer.prompt([
    {
      type: 'input',
      name: 'movieTitle',
      message: 'Enter the title or part of the title',
      validate: (input) => {
        if (input) return true;

        return 'Please enter a title!';
      }
    }
  ]);
  vorpal.log(chalk.green(JSON.stringify(movieTitleByUser, null, 2)));
} catch (error) {
  vorpal.log(chalk.red(error));
}

A lot is going on here so let’s go step by step.

We await the prompt inside a try/catch block. If an error occurs, the catch block will catch and display it in red with the help of chalk and vorpal.log.

The try part contains the more interesting part of the code.

Handling promises. Because inquirer.prompt returns a Promise, we need to handle it by unwrapping the promise and getting the value which the promise has been resolved with.

Before async/await this could be done using then but now it’s easier to use this syntax. await will make the value inside the promise available and we can save it into the movieTitleByUser variable.

The prompt. inquirer.prompt accepts the question object(s) wrapped in an array. Depending on the type of the input, the object can have different properties.

In our case the type of the prompt is input because we want the user to enter some characters of title of the movie they want more information about.

To help the user with the action we want them to take, we display a message. This message will be shown in the terminal when the search command is issued. In this case we ask the user to provide the title of the movie, or if they don’t know the exact title, just type some part of it.

Inquirer.js saves the user’s answer into an object and the name property of the question object determines which key the saved response should be assigned to inside the response object. In our case it should be movieTitle because the user response is just about title of the movie.

Finally, we can make some validation. The validate property is a function whose argument is the user input (input). If everything goes well and the user entered a valid title, the function returns true and prompt will save the user’s response.

If we don’t like what the user entered, we can return a message, so that the user can think their answer again.

We won’t go crazy here with the validation, so the only thing we’ll validate is that the user should enter something, i.e. an empty string is not acceptable (if (input) return true). In case of any other input we will save the answer.

Logging the answer. As mentioned above the user input is saved to an object as the value of the key given in the name property. We can log this object to the console in green but first it’s a good idea to JSON.stringify it to make more readable.

Let’s save our work and run npm start, then type search. First we should see the question with the blinking cursor at the end of the line:

? Enter the title or part of the title

Inquirer.js is prompting us to provide a title. First, press “Enter” and the error message we put inside the validate function will be seen:

>> Please enter a title!

The cursor stays next to the prompt, so we can now enter a title. I enter “Jason Bourne” but feel free to type something else. Your response will be logged to the console in green:

{
  "movieTitle": "Jason Bourne"
}

As we expected it’ll be an object, where the key (movieTitle) is what we provided for the name property and the value is our answer.

Add more questions

The API we’ll receive the data from (coming in the next post) has an option for the type of the movie as well. We can specify whether we want to look for a Movie, a particular Episode, a Series or it doesn’t matter (All).

Extending the prompt. We can add a second object to the prompt array:

{
  type: 'list',
  name: 'movieType',
  message: 'Select the type of the movie',
  choices: ['Movie', 'Series', 'Episode', 'All'],
  filter: (type) => type.toLowerCase()
}

This time we want the user to select from the options mentioned above. A suitable type would be list, where the user will see the options we present in the choices array as a list. We can select a suitable message for this question and will save the user response as the value of the movieType property.

Filter. Another property of the question object is filter, which (as its name suggests) filters the user’s choice and forwards the filtered result to the response object.

In this case, all we do is to convert the choice to lower case.

Now the object returned from prompt will contain two properties. Let’s see this in action, save the file, type exit in the terminal and start the script again with npm start and search.

The result. After you have entered the title of the movie, you should see something like this:

Movies$ search
? Enter the title or part of the title Jason Bourne
? Select the type of the movie (Use arrow keys)
> Movie
  Series
  Episode
  All

The first element of the choices array is listed first and the cursor is next to it. You can select the desired option with the arrow keys.

Let’s select All for now and we will get the object with both the title and the type selected:

{
  "movieTitle": "Jason Bourne",
  "movieType": "all"
}

As we can see the value of movieType has been converted to lower case.

Refactoring code

index.js is getting large as we add more code to it and even more will follow, so this is a good time to refactor the code and extract the prompt in its own module.

Create the module. Let’s create a new folder called modules in the project root and a file called user-inputs.js inside modules:

mkdir modules
cd modules
touch user-inputs.js

We will store all prompt-related functions in user-inputs.js.

Let’s require Inquirer.js at the top of the new file and at the same time, delete it from index.js

Create a new function called getMovieTitleFromUser in the new user-inputs.js file and copy the prompt:

const inquirer = require('inquirer');

const getMovieTitleFromUser = () => {
  return inquirer.prompt([
    {
      type: 'input',
      name: 'movieTitle',
      message: 'Enter the title or part of the title',
      filter: (title) => title.toLowerCase(),
      validate: (input) => {
        if (input) return true;

        return 'Please enter a title!';
      }
    },
    {
      type: 'list',
      name: 'movieType',
      message: 'Select the type of the movie',
      choices: ['Movie', 'Series', 'Episode', 'All'],
      filter: (type) => type.toLowerCase()
    }
  ]);
};

The function will return the prompt.

We also need to make this function available for index.js, so we need to export it:

module.exports = { getMovieTitleFromUser };

Getting the module. Now we need to access the prompt in index.js, so let’s require it at the top and await it at the right place:

// ..
const { getMovieTitleFromUser } = require('./modules/user-inputs');
// ..
try {
  const movieData = await getMovieTitleFromUser();
  vorpal.log(chalk.green(JSON.stringify(movieData, null, 2)));
} catch (error) {
  vorpal.log(chalk.red(error));
}
// ..

We can use object destructuring because it’s a really cool thing.

We await getMovieTitleFromUser inside the try block. With the await keyword we’ll receive the value which the prompt promise has been resolved with. In our case this will be the object with the user inputs (movieTitle and movieType), which we save into the variable called movieData for the time being.

If anything goes wrong, we’ll be directed to the catch block, where the error is logged to the console.

Title to lower case. To make life easier and more consistent, we can also convert the title of the movie to lower case.

The user can type the title in title case or lower case (or upper case, quite a few options are available); we don’t know for sure. We can though transfer the text the user entered; therefore our request to the API will be in lower case.

Let’s add a one-liner filter to the first object inside the prompt:

filter: (title) => title.toLowerCase(),

Run again. Let’s run the script again to make sure it works after refactoring the code. It should.

We’ll get an object displayed in the console where both the title and the type are lower cased.

The full code looks like this:

// index.js

const chalk = require('chalk');
const figlet = require('figlet');
const vorpal = require('vorpal')();

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

figlet('Movies', (_, data) => {
  console.log(chalk.cyan(data));

  vorpal
    .command('search', 'Start searching movies')
    .action(async () => {
      try {
        const movieData = await getMovieTitleFromUser();
        vorpal.log(chalk.green(JSON.stringify(movieData, null, 2)));
      } catch (error) {
        vorpal.log(chalk.red(error));
      }
    });

  vorpal
    .delimiter('Movies$')
    .show();
});

and

// modules/user-inputs.js

const inquirer = require('inquirer');

const getMovieTitleFromUser = () => {
  return inquirer.prompt([
    {
      type: 'input',
      name: 'movieTitle',
      message: 'Enter the title or part of the title',
      filter: (title) => title.toLowerCase(),
      validate: (input) => {
        if (input) return true;

        return 'Please enter a title!';
      }
    },
    {
      type: 'list',
      name: 'movieType',
      message: 'Select the type of the movie',
      choices: ['Movie', 'Series', 'Episode', 'All'],
      filter: (type) => type.toLowerCase()
    }
  ])
};

module.exports = { getMovieTitleFromUser };

Conclusion

Great! We have successfully integrated Inquirer.js into the CLI app, which is slowly getting its shape.

In the next post we’ll use the user input to fetch movie data from the API and will display real movie data in the terminal.

Thanks for reading and see you next time.