Navigation

Node.js CLI Tutorial

Overview

In this tutorial, you will use Node.js to create a task tracker command line interface (CLI) that allows users to:

  • Register themselves with email and password.
  • Sign in to their account with their email and password.
  • View a list of projects they are a member of.
  • View, create, modify, and delete tasks in projects.
  • View a list of team members in their project.
  • Add and remove team members to their project.

This tutorial should take around 30 minutes to complete.

Check Out the Quick Start

If you prefer to explore on your own rather than follow a guided tutorial, check out the Quick Start. It includes copyable code examples and the essential information that you need to set up a MongoDB Realm application.

Prerequisites

Before you begin, ensure you have:

Once you’re set up with these prerequisites, you’re ready to start the tutorial.

A. Clone the Client App Repository

We’ve already put together a task tracker CLI application that has most of the code you’ll need. You can clone the client application repository directly from GitHub:

git clone https://github.com/mongodb-university/realm-tutorial-node-js.git

Important

The final branch is a finished version of the app as it should look after you complete this tutorial. To walk through this tutorial, please check out the start branch:

git checkout start

In your terminal, run the following command to install its dependencies:

npm install

B. Explore the App Structure & Components

This application has a flat project structure: all of the files are in the root directory. Open a text editor to explore the directory and files. In this tutorial, we’ll be focusing on 5 files: config.js, users.js, tasks.js, team.js, projects.js. The other files provide the underlying structure for the CLI. The following table describes the role of important files in this project:

File Purpose
config.js Provides a single location for configuration data. You will put your Realm app ID here.
index.js The entry point for the app. Creates the Realm App that you app will use throughout its lifecycle and displays the initial logon screen.
main.js Displays the main menu of choices. Users can view a list of projects they are a member of or select a project.
output.js Responsible for displaying text in the terminal window.
tasks.js Handles all task-related communication between the CLI and Realm. The methods for listing, creating, editing, and deleting tasks live here.
schemas.js Contains the schema definitions for the collections used in this project.
users.js Handles Realm user authentication, including logging in, registering a new user, and logging out.
team.js Handles the team member related communication between the CLI and Realm. The methods for listing, adding, and removing team members are contained in this file.
projects.js Handles project retrieval and listing.

C. Connect to Your MongoDB Realm App

To get the app working with your backend, you first need to add your Realm App ID to the config.js file. The config.js module exports a single property, realmAppId, which is currently set to “TODO”:

exports.realmAppId = "TODO";

Change this value to your Realm App ID.

Note

To learn how to find your MongoDB Realm appId, read the Find Your App Id doc.

Once you have made that change, you now need to complete the code needed to open a realm. In index.js, find the openRealm function. Replace the TODO line with a line of code that opens a realm and assigns it to the realm property. It will look like this:

async function openRealm(partitionKey) {
  const config = {
    schema: [schemas.TaskSchema, schemas.UserSchema, schemas.ProjectSchema],
    sync: {
      user: users.getAuthedUser(),
      partitionValue: partitionKey,
    },
  };
  return Realm.open(config);
}

Now that you have implemented the openRealm function, you will now need to complete the code that retrieves the realm. In index.js, find the getRealm function. It will look like this:

async function getRealm(partitionKey) {
  if (realms[partitionKey] == undefined) {
    realms[partitionKey] = openRealm(partitionKey);
  }
  return realms[partitionKey];
}

At this point, your app is pointing to your backend and opens a connection to it when you start the app. However, users cannot log in yet, so let’s update that code next.

D. Enable authentication

In the users.js file, we have a logIn function that prompts the user for an email address and password, and then, within a try-catch block, creates an emailPassword credential and passes it to the Realm logIn() method.

Find the the logIn function and add the following code to create a emailPassword credential and call the logIn() method.

async function logIn() {
  const input = await inquirer.prompt([
    {
      type: "input",
      name: "email",
      message: "Email:",
    },
    {
      type: "password",
      name: "password",
      message: "Password:",
      mask: "*",
    },
  ]);

  try {
    const credentials = Realm.Credentials.emailPassword(
      input.email,
      input.password
    );

    const user = await app.logIn(credentials);
    if (user) {
      output.result("You have successfully logged in as " + app.currentUser.id);
      return main.mainMenu();
    } else {
      output.error("There was an error logging you in");
      return logIn();
    }
  } catch (err) {
    output.error(JSON.stringify(err, null, 2));
    return logIn();
  }
}

E. Implement the CRUD methods

In the tasks.js and projects.js files, there are a number of functions to handle typical CRUD functionality: getTasks, getTask, createTask, deleteTask, editTask, changeStatus, and getProjects. Each of these functions (except getTasks and getProjects) prompts the user for input and then makes the appropriate call to Realm. Your job is to implement the calls to Realm. The following list provides guidance on how to complete this task for each function.

In tasks.js:

  • getTasks

    To get all objects, call the objects() method and pass in the name of the collection:

    exports.getTasks = async (partition) => {
      const realm = await index.getRealm(partition);
      const tasks = realm.objects("Task");
      output.header("MY TASKS:");
      output.result(JSON.stringify(tasks, null, 2));
    };
    
  • getTask

    In the Tasks collection, a task’s id field is the primary key, so we call the objectForPrimaryKey() function to get a task by its Id.

    exports.getTask = async (partition) => {
      const realm = await index.getRealm(partition);
      try {
        const task = await inquirer.prompt([
          {
            type: "input",
            name: "id",
            message: "What is the task ID (_id)?",
          },
        ]);
        let result = realm.objectForPrimaryKey("Task", new bson.ObjectID(task.id));
        if (result !== undefined) {
          output.header("Here is the task you requested:");
          output.result(JSON.stringify(result, null, 2));
        }
      } catch (err) {
        output.error(JSON.stringify(err));
      }
    };
    
  • createTask

    Whenever we modify an object in realm, we must do so within a transaction. The write() method takes care of transaction handling for us. So, within the write function, we call the create() function, passing in all of the required properties:

    exports.createTask = async (partition) => {
      const realm = await index.getRealm(partition);
      try {
        output.header("*** CREATE NEW TASK ***");
        const task = await inquirer.prompt([
          {
            type: "input",
            name: "name",
            message: "What is the task text?",
          },
          {
            type: "rawlist",
            name: "status",
            message: "What is the task status?",
            choices: ["Open", "In Progress", "Closed"],
            default: function () {
              return "Open";
            },
          },
        ]);
        let result;
        realm.write(() => {
          result = realm.create("Task", {
            _id: new bson.ObjectID(),
            _partition: partition,
            name: task.name,
            status: task.status.replace(/\s/g, ''), // Removes space from "In Progress",
          });
        });
    
        output.header("New task created");
        output.result(JSON.stringify(result, null, 2));
      } catch (err) {
        output.error(JSON.stringify(err));
      }
    };
    

    Note

    The write function replaces the need to call the beginTransaction(), commitTransaction(), and cancelTransaction() methods.

  • deleteTask

    Deleting objects must also take place within a transaction. As with modifying an object, we’ll use the write() function to handle the transaction for us. We’ll first call the objectForPrimaryKey method to get the specific we want to delete and then the delete() function on that task:

    exports.deleteTask = async (partition) => {
      const realm = await index.getRealm(partition);
      output.header("DELETE A TASK");
      const answers = await inquirer.prompt([
        {
          type: "input",
          name: "id",
          message: "What is the task ID (_id)?",
        },
        {
          type: "confirm",
          name: "confirm",
          message: "Are you sure you want to delete this task?",
        },
      ]);
    
      if (answers.confirm) {
        let task = realm.objectForPrimaryKey("Task", new bson.ObjectID(answers.id));
        realm.write(() => {
          realm.delete(task);
          output.result("Task deleted.");
        });
        return;
      }
    };
    
  • modifyTask

    This function is called by both the editTask and changeStatus functions. Like the createTask and deleteTask methods, when you change an object, you do so within a transaction. Other than that, though, there is no specific call to a Realm API to change an object. Rather, you change the local object and Sync ensures the object is updated on the server.

    async function modifyTask(answers, partition) {
      const realm = await index.getRealm(partition);
      let task;
      try {
        realm.write(() => {
          task = realm.objectForPrimaryKey("Task", new bson.ObjectID(answers.id));
          task[answers.key] = answers.value;
        });
        return JSON.stringify(task, null, 2);
      } catch (err) {
        return output.error(err);
      }
    }
    

    Note

    To learn more about Realm Sync, see Sync Overview.

In projects.js:

  • getProjects

    As defined by our data model, projects are an embedded object of the users object. To get all projects the user is a part of, we’ll have to use the objectForPrimaryKey method to get the current user and then access the current user’s memberOf property.

    async function getProjects() { 
      const realm = await index.getRealm(`user=${users.getAuthedUser().id}`);
      const currentUser = users.getAuthedUser().id;
      const user = realm.objectForPrimaryKey("User", currentUser);
      const projects = user.memberOf;
      return projects;
    };
    

    Note

    To learn more about how Realm Object models are used in Node.js applications, see Realm Objects in our Node.js client guide.

F. Use Realm Functions

In the team.js file, there are functions that rely on Realm functions. Realm functions allow you to execute server-side logic for your client applications. Each of the following functions require you to implement the calls to Realm.

  • getTeamMembers

    To get all team members, call the getMyTeamMembers Realm function using the User.functions method.

    exports.getTeamMembers = async () => {
      const currentUser = users.getAuthedUser();
      try {
        const teamMembers = await currentUser.functions.getMyTeamMembers();
        output.result(JSON.stringify(teamMembers, null, 2));
      }
      catch (err) {
        output.error(JSON.stringify(err));
      }
    }; 
    
  • addTeamMember

    This function prompts the user for the email of the new team member. You will need to call the addTeamMember Realm function and pass it the email parameter.

    exports.addTeamMember = async () => {
      try {
        output.header("*** ADD A TEAM MEMBER ***");
        const currentUser = users.getAuthedUser();
        const { email } = await inquirer.prompt([
          {
            type: "input",
            name: "email",
            message: "What is the new team member's email address?",
          },
        ]);
        await currentUser.functions.addTeamMember(email);
        output.result("The user was added to your team.");
      } catch (err) {
        output.error(err.message);
      }
    };
    
  • removeTeamMember

    This functions prompts the user for the email of the team member they would like to remove from their project. You will need to call the removeTeamMember Realm function and pass it the email parameter.

    exports.removeTeamMember = async () => {
      const currentUser = users.getAuthedUser();
      const teamMembers = await currentUser.functions.getMyTeamMembers();
      const teamMemberNames = teamMembers.map(t => t.name);
      try {
        output.header("*** REMOVE A TEAM MEMBER ***");
        const { selectedTeamMember } = await inquirer.prompt([
          {
            type: "rawlist",
            name: "selectedTeamMember",
            message: "Which team member do you want to remove?",
            choices: [...teamMemberNames, new inquirer.Separator()],
          },
        ]);
        let result = await currentUser.functions.removeTeamMember(selectedTeamMember);
        output.result("The user was removed from your team.");
      } catch (err) {
        output.error(JSON.stringify(err));
      }
    };
    

H. Run and Test

Once you have completed the code, you should run the app and check functionality.

  1. Open a terminal window and change to your app’s directory.

  2. Run the following commands to install all of the dependencies and start the app:

    npm install
    node index.js
    
  3. Your terminal window will clear and you will see the initial menu prompting you to log in or register as a new user:

    Initial menu

If the app builds successfully, here are some things you can try in the app:

  • Create a user with email first@example.com
  • Explore the app, then log out.
  • Start up the app again and register as another user with email second@example.com
  • Select second@example.com’s project
  • Add, update, and remove some tasks
  • Select the “Manage Team” menu option
  • Add first@example.com to your team
  • Log out and log in as first@example.com
  • See two projects in the projects list
  • Navigate to second@example.com’s project
  • Collaborate by adding, updating, and removing some new tasks

Reminder

If something isn’t working for you, you can check out the final branch of this repo to compare your code with our finished solution.

What’s Next?

You just built a functional task tracker web application built with MongoDB Realm. Great job!

Now that you have some hands-on experience with MongoDB Realm, consider these options to keep practicing and learn more:

Leave Feedback

How did it go? Please let us know if this tutorial was helpful or if you had any issues by using the feedback widget on the bottom right of the page.