How To Build A Cli App In Node.js

Added 5 months ago By Stephen Ilori

Tags

My name is Ilori Stephen and I am a full-stack software developer in West Africa Nigeria. In this tutorial, I will be showing you how to build a Cli App using Node.js.

Node.js is a javascript runtime built on chrome's v8 engine. But in this lesson, I'll introduce you to Node.js by building a mini CLI App.

Why Would You Want To Use Node Or Even Build A Cli App?

Node.js is a platform built on Chrome's JavaScript runtime for easily building fast and scalable network applications.

Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.

With that said, I am sure you'd definitely want to learn Node.js but why would you want to build a CLI App or a CLI Function still?

A Cli stands for Command Line Interface, meaning it is a text-based interface used for entering commands to interact with the computer. In the early days of computing, it was the standard way of interacting with the computer.

Sounds awesome right? we will be getting to that later in this chapter.

Glossary

While I was preparing this lecture, I came across different terms or tech jargons related to Node.js for this lesson.

  1. NPM: Npm stands for Node Package Manager and it is the default package manager for the Javascript runtime environment (Node.js).

  2. Modules: A module is part of a program, and programs are composed of one or more independently developed modules that are not combined until the program is linked.

  3. Cli: A CLI stands for Command Line Interface and it is a text-based interface for interacting with the computer. A Cli would be your CMD in Windows, and Terminal in Linux.

  4. CRUD: In computer programming, create, read, update, and delete (CRUD) are the four basic functions of persistent storage. Read more

Project Requirements

In order to get the best out of this lecture, the following requirements need to be satisfied.

  1. Node.js: The most recent version of Node.js installed or at least, an LTS version of Node.js downloaded from their website Node.js.

If you are unsure about having Node.js installed on your computer, you can open your terminal and run the command node -v

  1. Npm Installed: The most recent version of Npm should be installed, there are several package managers for javascript, but for this lesson, I recommend Npm. You can verify its installation by running npm -v in your Terminal or CMD.

  2. A Text Editor: A text editor to write your codes with. You can download either Visual studio code, Atom, or Bracket.

  3. Basic Javascript Knowledge: In order to get the best out of this lecture, you need to have a basic or intermediate knowledge of javascript.

  4. MongoDB: MongoDB installed locally on your system. You can however use a remote instance of MongoDB for this lesson provided you know how to walk around the configurations.

  5. OpenWeather API Key: An API key from the openweather website for consuming their API and getting weather information about a particular city.

With the requirements all satisfied, we can then now begin building our Cli App. We would begin by creating a new folder. You can name the folder whatever you want but for this lesson, I am sticking to node-cli-app. Inside of the just created folder, we should have a simple working directory that looks like the following.

Project Directory

*/ node-cli-app (Project)
    */ app (Folder. Our Application logic lives here.)
        /Notes.js
        /Weather.js
    index.js
    package.json

1. Editing Our Package.json File (Root Dir)

In the root directory of our project lives our package.json file. It holds sensitive information about our project by storing all of our project dependencies and some Terminal commands for interacting with our application. With that said, your package.json file should look like the following.

{
  "name": "node-cli-app",
  "version": "1.0.0",
  "description": "A cli app in node js for getting details about the weather.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/learningdollars/ilori-cli-node.git"
  },
  "keywords": [
    "Cli",
    "Weather"
  ],
  "author": "Ilori Stephen A",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/learningdollars/ilori-cli-node/issues"
  },
  "homepage": "https://github.com/learningdollars/ilori-cli-node#readme",
  "dependencies": {
    "axios": "^0.19.2",
    "lodash": "^4.17.15",
    "mongodb": "^3.5.7",
    "yargs": "^15.3.1"
  }
}

After that, you should open your project up in Terminal, CMD or shell and run the command npm install.

This will create a node_modules folder in the root directory of your project and install some dependencies for our project. Remember the term Modules we spoke about in the Glossary of this chapter? Well, more on that later.

2. Editing Our Index.js File (Root Dir)

In the root directory of our project, we have an index.js file which serves as an entry point into our application. It calls different methods based on the command that was executed from the terminal. With that said, The index.js file should look like the following.

//Node Modules....
const Yargs = require('yargs');

//Custom Modules....
const Notes = require('./app/Notes');
const Weather = require('./app/Weather');

//App Initialization...
const YargsArgV = Yargs
  .command('add', 'Adds A New Note', {
    title: {
      alias: 't',
      demand: true,
      describe: 'Add a title for your new Note.',
    },
    body: {
      alias: 'b',
      demand: true,
      describe: 'Add a body for your new Note.'
    }
  })
  .command('list', 'List all notes.', {
    status: {
      alias: 's',
      demand: false,
      describe: 'Fetches all notes with a status type of completed and pending'
    }
  })
  .command('fetchNote', 'Fetch a particular Note.', {
    title: {
      alias: 't',
      demand: true,
      describe: 'Fetch a Note with this title.'
    }
  }).command('completeNote', 'Completes a Note with this title', {
    title: {
      alias: 't',
      demand: true,
      describe: 'Marks a Note with a status of completed.'
    }
  }).command('updateNote', 'Updates a Note with this title', {
    title: {
      alias: 't',
      demand: true,
      describe: 'Updates a Note with this title.'
    },
    body: {
      alias: 'b',
      demand: true,
      describe: 'A New content for the note.'
    }
  })
  .command('deleteNote', 'Deletes a particular Note', {
    title: {
      alias: 't',
      demand: true,
      describe: 'Deletes a Note with this title.'
    }
  }).command('fetchWeather', 'Fetches information about the weather for a specific city', {
    city: {
      alias: 'c',
      demand: true,
      describe: 'Fetches information about the weather for this city.'
    }
  })
  .help().argv;

//Fetch cli command...
const Command = YargsArgV._[0];

//Cli commands...
if (Command === 'add')
  Notes.createNote( YargsArgV.title, YargsArgV.body );
else if (Command === 'list')
  Notes.listNotes( YargsArgV.status );
else if (Command === 'fetchNote')
  Notes.fetchNote( YargsArgV.title );
else if (Command == 'completeNote')
  Notes.completeNote( YargsArgV.title );
else if (Command == 'updateNote')
  Notes.updateNote( YargsArgV.title, YargsArgV.body );
else if (Command === 'deleteNote')
  Notes.deleteNote( YargsArgV.title );
else if (Command == 'fetchWeather')
  Weather.getWeather( YargsArgV.city );
else
  console.log( 'Command not recognized...' );

The Modules

I spoke about getting back to the Modules term earlier. well, now is the time. ta-da!!!!. The term Modules if you ask me is a separate part of a program that sums up to make the program itself.

In Node.js, a separate piece of code or file that you require is called a Module. This is more like the require or include method from your PHP language.

It includes or requires some methods or functions into the current scope of your project.

If you check the package.json file again, You will find a dependencies Object with several keys there.

Note: You require modules installed via npm with require and the Module name as it is in the dependencies Object inside of the package.json file. You require other files as modules by calling its path inside of the require method.

The Yargs Module

The Yargs Module exposes some sane methods for reading arguments passed from the CLI or building interactive command-line tools. For me, it is as simple as that.

We created some commands for our application by using the Yargs constant and chaining several command method to it. This makes our program more interactive by explaining what a command does when we run it with the --help flag.

The command method accepts 3 arguments.

  • The Name of the Command,
  • A description for the command
  • An Object of arguments for the command.

I hope that wasn't all too confusing. In the next line after the chains, we created a command constant by reusing YargsArgV constant and getting the first index from the _ key..

After that, we create a conditional that checks what type of command was passed from the terminal and executes several methods depending on the command.

The methods being executed inside of the conditionals are the methods from the Notes Class and the Weather Class from the Notes Module and the Weather Module in the app directory.

With that said, let's get to editing the files inside of our app directory.

3. Editing The Notes.js File (App directory)

This file houses a Class that provides a higher level of abstraction by exposing methods that are consumed inside the index.js file of our application. The file also depends on the MongoDb module and the Lodash module. With that said, our Notes.js should look like the following.

const { MongoClient } = require('mongodb');
const _ = require('lodash');

class Notes {
  constructor() {  }

  // Create A New Mongo Connection...
  async createConnection() {
    // Mongo Config || Constants.....
    const Url = 'mongodb://localhost:27017';
    const dbName = 'learning_dollars_db';
    // Mongo Initialization....
    const Client = await new MongoClient(Url, { useUnifiedTopology: true });
    const Database = new Promise((resolve, reject) => {
        Client.connect((err) => {
          if (err) {
            reject(err);
          }

          resolve(Client.db(dbName));
        });
    });

    return Database;
  }

  // Create A New Note Entry...
  async createNote(noteTitle, noteBody) {
    let title = _.trim(noteTitle);
    title = _.upperFirst(title);

    let body = _.trim(noteBody);
    body = _.upperFirst(body);

    // check if the title exist....
    const CheckTitle = await this.checkNote(title);
    if (CheckTitle) {
      console.log('*********** NOTE ***********');
      console.log('Sorry, But A Note with this title already exist.');
      console.log('*********** NEW NOTE ***********');
      return;
    }

    // Create the note...
    const dbConnection = await this.createConnection();
    try {
      const NewNote = await dbConnection.collection('notes_db').insertOne({ title: title, body: body, completed: false }).then((doc) => {
        return doc.ops[0];
      });

      if (NewNote) {
        console.log('*********** NEW NOTE ***********');
        console.log(`Title: ${note.title}`);
        console.log(`Body: ${note.body}`);
        console.log(`Completed: ${note.completed}`);
        console.log('*********** NEW NOTE ***********');
      }
    } catch (e) {
        console.log('*********** Error ***********');
        console.log(`ErrorName: ${e.name}`);
        console.log(`errorMessage: ${e.message}`);
        console.log('*********** Error ***********');
    }

    return;
  }

  // List Notes Depending On The Status...
  async listNotes(status = null) {
    const dbConnection = await this.createConnection();
    switch (status) {
      case 'completed':
        try {
          const CompletedNotes = await dbConnection.collection('notes_db').find({ completed: true }).toArray().then((docs) => {
            return docs;
          });

          if (CompletedNotes.length > 0) {
            console.log('*********** Completed NOTES **************');
            CompletedNotes.forEach((note, index) => {
              console.log(`************* Index: ${index} ********************`);
              console.log(`Title: ${note.title}`);
              console.log(`Body: ${note.body}`);
              console.log(`Completed: ${note.completed}`);
            });
            console.log('*********** Completed NOTES **************');
          } else {
            console.log('*********** There Are No Completed Notes Yet **************');
          }
        } catch (e) {
            console.log('*********** Error ***********');
            console.log(`ErrorName: ${e.name}`);
            console.log(`errorMessage: ${e.message}`);
            console.log('*********** Error ***********');
        }
        break;
      case 'pending':
        try {
          const PendingNotes = await dbConnection.collection('notes_db').find({ completed: false }).toArray().then((docs) => {
                return docs;
              });

          if (PendingNotes.length > 0) {
          console.log('*********** Pending NOTES **************');
            PendingNotes.forEach((note, index) => {
              console.log(`************* Index: ${index} ********************`);
              console.log(`Title: ${note.title}`);
              console.log(`Body: ${note.body}`);
              console.log(`Completed: ${note.completed}`);
            });
            console.log('*********** Pending NOTES **************');
          } else {
            console.log('*********** There Are No Pending Notes Yet **************');
          }
        } catch (e) {
            console.log('*********** Error ***********');
            console.log(`ErrorName: ${e.name}`);
            console.log(`errorMessage: ${e.message}`);
            console.log('*********** Error ***********');
        }
        break;
      default:
         try {
           const AllNotes = await dbConnection.collection('notes_db').find().toArray().then((docs) => {
             return docs;
           });

           if (AllNotes.length > 0) {
             console.log('*********** All NOTES **************');
             AllNotes.forEach((note, index) => {
               console.log(`************* Index: ${index} ********************`);
               console.log(`Title: ${note.title}`);
               console.log(`Body: ${note.body}`);
               console.log(`Completed: ${note.completed}`);
             });
             console.log('*********** All NOTES **************');
           } else {
             console.log('*********** There Are No Notes Yet **************');
           }
         } catch (e) {
             console.log('*********** Error ***********');
             console.log(`ErrorName: ${e.name}`);
             console.log(`errorMessage: ${e.message}`);
             console.log('*********** Error ***********');
         }
        break;
    }
    return;
  }

  // Fetch A Note that matches this title...
  async fetchNote(title) {
    // Fetch Note...
    const dbConnection = await this.createConnection();

    try {
      const Note = await dbConnection.collection('notes_db').findOne({ title }).then((doc) => {
        return doc;
      });

      if (Note) {
        console.log('*********** NOTE ***********');
        console.log(`Title: ${Note.title}`);
        console.log(`Body: ${Note.body}`);
        console.log(`Completed: ${Note.completed}`);
        console.log('*********** NOTE ***********');
      } else {
        console.log('*********** The Note With This Title Could Not Be Found **************');
      }
    } catch (e) {
      console.log('*********** Error ***********');
      console.log(`ErrorName: ${e.name}`);
      console.log(`errorMessage: ${e.message}`);
      console.log('*********** Error ***********');
    }

    return;
  }

  // Mark A Note as completed...
  async completeNote(title) {
    // check if the title exist....
    const CheckTitle = await this.checkNote(title);
    if (!CheckTitle) {
      console.log('*********** NOTE ***********');
      console.log('Sorry, But A Note with this title could not be found.');
      console.log('*********** NOTE ***********');
      return;
    }

    // Complete Note....
    const dbConnection = await this.createConnection();
    try {
      const Note = await dbConnection.collection('notes_db').findOneAndUpdate({
        title
      }, {
        $set: {
          completed: true
        }
      }, {
        returnOriginal: false
      }).then((note) => {
        return note.value;
      });

      if (Note) {
        console.log('*********** NOTE ***********');
        console.log(`Title: ${Note.title}`);
        console.log(`Body: ${Note.body}`);
        console.log(`Completed: ${Note.completed}`);
        console.log('*********** NOTE ***********');
      }
    } catch (e) {
        console.log('*********** Error ***********');
        console.log(`ErrorName: ${e.name}`);
        console.log(`errorMessage: ${e.message}`);
        console.log('*********** Error ***********');
    }

    return;
  }

  // Update A Note....
  async updateNote(title, body) {
    // check if the title exist....
    const CheckTitle = await this.checkNote(title);
    if (!CheckTitle) {
      console.log('*********** NOTE ***********');
      console.log('Sorry, But A Note with this title could not be found.');
      console.log('*********** NEW NOTE ***********');
      return;
    }

    // Update Note...
    const dbConnection = await this.createConnection();
    try {
      const Note = await dbConnection.collection('notes_db').findOneAndUpdate({
        title
      }, {
        $set: {
          body: body
        }
      }, {
        returnOriginal: false
      }).then((doc) => {
        return doc.value;
      });

      if (Note) {
        console.log('*********** NOTE ***********');
        console.log(`Title: ${Note.title}`);
        console.log(`Body: ${Note.body}`);
        console.log(`Completed: ${Note.completed}`);
        console.log('*********** NOTE ***********');
      }
    } catch (e) {
        console.log('*********** Error ***********');
        console.log(`ErrorName: ${e.name}`);
        console.log(`errorMessage: ${e.message}`);
        console.log('*********** Error ***********');
    }

    return;
  }

  // Delete A Note entry...
  async deleteNote(title) {
    // check if the title exist....
    const CheckTitle = await this.checkNote(title);
    if (!CheckTitle) {
      console.log('*********** NOTE ***********');
      console.log('Sorry, But A Note with this title could not be found.');
      console.log('*********** NEW NOTE ***********');
      return;
    }

    // Delete Note....
    const dbConnection = await this.createConnection();
    await dbConnection.collection('notes_db').deleteOne({ title }).then((doc) => {
      if (doc.result.ok) {
        console.log('*********** Delete NOTE ***********');
        console.log('The selcted Note has been deleted successfully.');
        console.log('*********** Delete NOTE ***********');
      }
    }).catch((e) => {
      console.log('*********** Error ***********');
      console.log(`ErrorName: ${e.name}`);
      console.log(`errorMessage: ${e.message}`);
      console.log('*********** Error ***********');
    });
    return;
  }

  async checkNote(title) {
    const dbConnection = await this.createConnection();
    return dbConnection.collection('notes_db').findOne({ title }).then((doc) => {
      if (doc) {
        return true;
      } else {
        return false;
      }
    }).catch((e) => {
      console.log('*********** Error ***********');
      console.log(`ErrorName: ${e.name}`);
      console.log(`errorMessage: ${e.message}`);
      console.log('*********** Error ***********');
    });
  }
}

module.exports = new Notes();

The Constructor

I forgot to mention earlier. Another reason why Node.js is so awesome is that it allows you to make use of the latest methods or syntax introduced to javascript. Our constructor method here doesn't do much in fact, it's actually empty.

The createConnection Method

This asynchronous method creates a new MongoDb connection and returns a promise depending on the outcome of the operation. The promise or the connection is now then reused by other methods in the Notes Class.

The createNote Method

This asynchronous method reuses the createConnection method and it accepts two-parameter and creates a new note by using the parameters provided. However, an exception is thrown if a note with the title exists by querying the database.

If a note with the title does not exist, the program continues to create the note and prints out default information about the note to the terminal.

The listNotes Method

This asynchronous method reuses the createConnection method and accepts one parameter with a default value set to null. This method returns and prints a list of notes to the terminal depending on the status passed in as an argument.

The fetchNote Method

This asynchronous method reuses the createConnection method and accepts one parameter which is the title of the note to fetch.

However, an exception is thrown if a note with the title could not be found. Else, it goes on to print some information about the note to the terminal.

The completeNote Method

This asynchronous method reuses the createConnection method and it accepts one parameter which is the title of the note to complete. An exception is thrown which logs some information to the terminal if a note with that title could not be matched in the database. Else, it goes on to mark the note as completed and prints some information about the note to the terminal.

The updateNote Method

This asynchronous method reuses the createConnection method and it accepts two parameters which are the title of the note and the body of the note. This method updates the body of a note that matches the title of the note.

However, if a note with the title could not be found, the program ends the execution of the method and prints some information to the terminal.

The deleteNote Method

This asynchronous method reuses the createConnection method and it accepts one parameter which is the title of the note to be deleted from the database.

However, if a note with the title could not be found, the program ends the execution of the method and prints some information to the terminal.

The checkNote method

This asynchronous method reuses the createConnection method and it accepts one parameter which is the title of the note to match. The method returns a boolean value which is either true if a match is found else false.

At the very end of the script, we make use of Node.js Module.exports which creates and exports a new instance of the Notes class.

This is what makes it possible to reuse the Note class methods in other files such as the index.js file in the root directory of our project.

4. Editing The Weather.js File (App directory)

This file houses a Weather Class that makes use of the Axios module to consume the openWeather API for getting information about the weather of a particular city. With that said and done, the weather.js file should look like the following.

const Axios = require('axios');

class Weather {
  constructor() { }

  async getWeather(cityName = 'Lagos') {
    const ApiKey = 'YOUR_OPEN_WEATHER_API_KEY';
    const Url = `http://api.openweathermap.org/data/2.5/weather?q=${cityName}&appid=${ApiKey}`;

    await Axios.get(Url)
    .then(function (resp) {
      let response = resp.data;
      console.log('************** Current Weather ******************');
      console.log(`Description: ${response.weather[0].main}`);
      console.log('**************************************************');
      console.log(`Minimum Temperature: ${response.main.temp_min}`);
      console.log('**************************************************');
      console.log(`Maximum Temperature: ${response.main.temp_max}`);
      console.log('**************************************************');
      console.log(`Feels Like: ${response.main.feels_like}`);
      console.log('************** Current Weather ******************');
    })
    .catch(function (error) {
      console.log(error);
    });
    return;
  }
}

module.exports = new Weather();

With that all said and done, you can begin interacting with the application by running the command node index.js --help. This will display some helper methods about how to use the application.

You can also run the command node index.js createNote --t="First Note" --body="This is my first note with the CLI in node.js. This will create your first note.

The getWeather Method

This asynchronous method accepts one parameter with a default value of Lagos as the city name. This method consumes the openweather API and returns a JSON response about the weather information about the city passed in as an argument.

With this example, you just made your very first cli app that makes an HTTP request. Pretty Impressive huh! Just make sure you grab your API Key from their website.

Learning Tools

There are a lot of learning tools online. But in case you are looking, I recommend you visit the following URL.

  1. Node.js Official Website Node.js. This is the #1 goto place to learn how Node.js works. It holds a whole lot of documentation which explains different methods in Node.js

  2. Brad Traversy is my #1 goto mentor to learn any new programming language or how to create features in any programming language. visit the URL Youtube.

Learning Strategy

I used the learning tools above and a few PDF documentations to achieve this. Anytime I faced some bugs, I would use stack overflow to check for solutions. But watching those videos helped a lot. I recommend that you do also.

Reflective Analysis

It was a simple process for me but I was able to gain some deeper insights into Node.js and programming in general.

In order to get more knowledge, I recommend that you log every command executed by a user and the current timestamp to a file inside of a folder called logs. This would give you some deeper understanding of how Node.js filesystem work.

You could also improvise heavily on the application by creating a CRON Job that sends a mail to you whenever a task is due to its completion date. That would require that you modify the database as well.

Conclusion

This project mainly focuses on building a CLI app in Node.js. But we took advantage of this by making notes from the CLI and also some HTTP requests.

If you ever thought Node.js was scary or something, I used to think the same way not until I realize that it's just javascript but in a different environment.

You can get the best of Node.js by extending this application further. I believe in you to create something great. Once again, my name is Ilori Stephen.

Get the complete project from github.

How To Build A Cli App In Node.js was Authored by Stephen Ilori
Get to know more about Stephen Ilori
Author's Avatar

Stephen Ilori

I am a fullstack web developer in Lagos igeria with 1 + year working experience. I enjoy building software applications for both myself and my firm.

See All Stephen Ilori Articles

All Tags

If you are looking for more, browse through our available tags.

Frontend Backend Fullstack Devops Mobile Career
1
288
1

Subscribe To Our Newsletter

Subscribe to our newsletter... Get weekly helpful tips on programming and video updates... We promise never to bug you. Just about two to three messages per week.