Build a Simple Node.js Command-Line To-Do App

Written by

in

Tired of juggling tasks in your head or relying on clunky, over-featured to-do list apps? Wouldn’t it be great to have a simple, quick way to manage your tasks directly from your terminal? In this tutorial, we’ll dive into building a command-line to-do application using Node.js. This project is perfect for beginners and intermediate developers looking to sharpen their Node.js skills, understand how to work with the command line, and learn about file system operations.

Why Build a Command-Line To-Do App?

Command-line applications, or CLIs, might seem old-school, but they offer a streamlined way to interact with your computer. They’re fast, efficient, and often less distracting than graphical interfaces. Building a CLI to-do app provides several benefits:

  • Efficiency: Quickly add, view, and manage tasks without leaving your terminal.
  • Learning: It’s a great exercise to understand Node.js, file system interactions, and command-line arguments.
  • Customization: Tailor the app to your exact needs and preferences.
  • Portability: Runs on any system with Node.js installed.

This tutorial will guide you step-by-step through the process, explaining each concept in simple terms with plenty of code examples.

Prerequisites

Before we begin, make sure you have the following installed:

  • Node.js and npm (Node Package Manager): You can download them from nodejs.org.
  • A text editor or IDE: Such as VS Code, Sublime Text, or Atom.
  • Basic understanding of JavaScript: Familiarity with variables, functions, and arrays will be helpful.

Project Setup

Let’s start by setting up our project. Open your terminal and navigate to the directory where you want to create your project. Then, run the following commands:

mkdir node-todo-cli
cd node-todo-cli
npm init -y

The mkdir command creates a new directory named node-todo-cli. The cd command changes your current directory to the newly created directory. The npm init -y command initializes a new Node.js project. The -y flag accepts all the default settings.

This will create a package.json file in your project directory. This file holds metadata about your project and lists any dependencies.

Creating the To-Do App’s Core Logic

Now, let’s create the core functionality of our to-do app. We’ll start by creating a file named todo.js in the root directory of your project. This file will contain the logic for adding, listing, and completing tasks.

Here’s the basic structure of the todo.js file:


// todo.js
const fs = require('fs');
const path = require('path');

const filePath = path.join(__dirname, 'todos.json');

// Function to read tasks from the file
function readTasks() {
  try {
    const data = fs.readFileSync(filePath, 'utf8');
    return JSON.parse(data);
  } catch (error) {
    // If the file doesn't exist or is empty, return an empty array
    return [];
  }
}

// Function to write tasks to the file
function writeTasks(tasks) {
  fs.writeFileSync(filePath, JSON.stringify(tasks, null, 2));
}

// Add a new task
function addTask(task) {
  const tasks = readTasks();
  tasks.push({ text: task, completed: false });
  writeTasks(tasks);
  console.log(`Task "${task}" added.`);
}

// List all tasks
function listTasks() {
  const tasks = readTasks();
  if (tasks.length === 0) {
    console.log('No tasks yet.');
    return;
  }

  tasks.forEach((task, index) => {
    const status = task.completed ? '[x]' : '[ ]';
    console.log(`${index + 1}. ${status} ${task.text}`);
  });
}

// Complete a task
function completeTask(index) {
  const tasks = readTasks();
  if (index >= 0 && index = 0 && index < tasks.length) {
    const deletedTask = tasks.splice(index, 1)[0];
    writeTasks(tasks);
    console.log(`Task "${deletedTask.text}" deleted.`);
  } else {
    console.log('Invalid task number.');
  }
}

// Export the functions to be used in the command-line interface
module.exports = {
  addTask,
  listTasks,
  completeTask,
  deleteTask,
};

Let’s break down this code:

  • Importing Modules: We import the fs (file system) and path modules. The fs module allows us to interact with the file system (reading and writing files), and the path module helps us work with file paths.
  • File Path: We define filePath to store the path to our todos.json file, where we’ll save our tasks. We use path.join(__dirname, 'todos.json') to create an absolute path to the file, ensuring it works correctly regardless of where the script is run.
  • readTasks() Function: This function reads the tasks from the todos.json file. It uses fs.readFileSync() to read the file’s contents. If the file doesn’t exist or there’s an error, it returns an empty array to prevent errors. We use a try-catch block to handle potential errors when reading the file.
  • writeTasks(tasks) Function: This function writes the tasks to the todos.json file. It uses fs.writeFileSync() to write the tasks to the file. The tasks are converted to a JSON string using JSON.stringify(). The null argument is for replacer (we don’t need a replacer here), and the 2 argument adds indentation for better readability.
  • addTask(task) Function: This function adds a new task to the list. It reads the existing tasks, adds the new task to the array, and then writes the updated array back to the file.
  • listTasks() Function: This function lists all the tasks. It reads the tasks from the file and then iterates through them, printing each task to the console. It also indicates whether a task is complete.
  • completeTask(index) Function: This function marks a task as complete. It takes the index of the task as an argument. If the index is valid, it updates the task’s completed property to true and writes the updated array back to the file.
  • deleteTask(index) Function: This function deletes a task from the list. It takes the index of the task as an argument. If the index is valid, it removes the task from the array using splice() and writes the updated array back to the file.
  • module.exports: We export the functions to make them accessible from other files.

Creating the Command-Line Interface (CLI)

Now, let’s create the command-line interface. Create a new file named index.js in your project directory. This file will handle the command-line arguments and call the appropriate functions from todo.js.


// index.js
const { addTask, listTasks, completeTask, deleteTask } = require('./todo');

const args = process.argv.slice(2);
const command = args[0];

switch (command) {
  case 'add':
    const task = args.slice(1).join(' ');
    if (!task) {
      console.log('Please provide a task.');
      break;
    }
    addTask(task);
    break;
  case 'list':
    listTasks();
    break;
  case 'complete':
    const completeIndex = parseInt(args[1]) - 1;
    completeTask(completeIndex);
    break;
  case 'delete':
    const deleteIndex = parseInt(args[1]) - 1;
    deleteTask(deleteIndex);
    break;
  default:
    console.log('Invalid command. Use: add [task], list, complete [index], delete [index]');
}

Let’s break down this code:

  • Importing Functions: We import the functions we defined in todo.js.
  • Processing Arguments: process.argv is an array containing the command-line arguments. The first two elements are always the path to the Node.js executable and the path to the script itself. We use process.argv.slice(2) to get an array of arguments passed to our script.
  • Command Handling: We use a switch statement to handle different commands.
    • ‘add’: Extracts the task from the arguments and calls the addTask() function. It also checks if a task is provided.
    • ‘list’: Calls the listTasks() function.
    • ‘complete’: Extracts the index of the task to complete, converts it to a number, and calls the completeTask() function. It subtracts 1 from the index because array indices start from 0.
    • ‘delete’: Extracts the index of the task to delete, converts it to a number, and calls the deleteTask() function. It subtracts 1 from the index because array indices start from 0.
    • default: Displays a help message if an invalid command is entered.

Running the Application

To run your application, open your terminal, navigate to your project directory (node-todo-cli), and use the following commands:

  • Add a task: node index.js add "Buy groceries"
  • List tasks: node index.js list
  • Complete a task: node index.js complete 1 (assuming the first task is “Buy groceries”)
  • Delete a task: node index.js delete 1 (assuming the first task is “Buy groceries”)

You can adjust the task names and index numbers as needed. The application will store your tasks in a file named todos.json in the project directory.

Error Handling and Common Mistakes

Error handling is crucial for creating robust applications. Let’s look at some common mistakes and how to fix them:

  • File Not Found Errors: If the todos.json file is not found, the application might crash. To prevent this, ensure that the file exists before attempting to read from it. Our readTasks() function already handles this by returning an empty array if the file doesn’t exist.
  • Invalid Input: Users might enter invalid input, such as non-numeric values for task indices. Always validate user input to prevent errors. We check for valid indices in the completeTask() and deleteTask() functions.
  • Incorrect File Paths: Using hardcoded file paths can cause problems. Always use relative paths or the path module to create dynamic file paths. We’ve used path.join(__dirname, 'todos.json') for a reliable path.
  • Missing Command-Line Arguments: Users might forget to provide the necessary arguments. Provide clear error messages to guide the user. We added checks for missing tasks in the ‘add’ command.

Enhancements and Future Improvements

Here are some ideas to enhance your to-do app:

  • User Interface: While this is a command-line app, you could explore using a library like inquirer to create interactive prompts and a more user-friendly interface.
  • Date and Time: Add due dates and times to your tasks.
  • Priorities: Allow users to set priorities for their tasks (e.g., high, medium, low).
  • Categories/Tags: Implement categories or tags to organize tasks.
  • Persistence: Consider using a database (like SQLite) for more robust data storage, especially if you plan to add more features and data.
  • Error Handling: Implement more comprehensive error handling, such as logging errors to a file.
  • Testing: Write unit tests to ensure the functionality of your application.

Key Takeaways

  • Node.js CLI Development: You’ve learned how to create a basic command-line application using Node.js.
  • File System Operations: You’ve gained experience reading from and writing to files using the fs module.
  • Command-Line Argument Parsing: You’ve learned how to process command-line arguments using process.argv.
  • Modularity: You’ve seen how to structure your code using modules to improve readability and maintainability.

FAQ

  1. How do I install Node.js? You can download Node.js from the official website: nodejs.org. The installation process is straightforward, and it also installs npm (Node Package Manager).
  2. Where are my tasks stored? Your tasks are stored in a file named todos.json in your project directory.
  3. How can I add multiple words to a task? When adding a task, enclose the task in quotes, for example: node index.js add "Grocery shopping".
  4. What if I get an error when running the app? Check the error message in your terminal. Common issues include incorrect file paths, missing modules, or syntax errors. Double-check your code and ensure all dependencies are installed.
  5. Can I use this app on my mobile? Since this is a command-line application, it won’t work natively on a mobile device. However, you could explore using a terminal emulator on your mobile device if you want to run it on the go.

Building this simple to-do app provides a solid foundation for understanding Node.js and building command-line applications. With the knowledge gained, you can now explore more complex projects and continue to grow your skills. The ability to manipulate files, parse arguments, and create interactive elements opens up a world of possibilities for automating tasks and building custom tools. From here, you can take your CLI skills further, crafting more sophisticated tools that streamline your workflow and boost your productivity. The principles of modularity, error handling, and user input validation learned here are transferrable to any Node.js project.