Let's Build a Todo CLI With Node.JS - Part Two
Let's Build a Todo CLI With Node.JS - Part Two

In this second course in the Let's Build a Todo CLI With Node.JS series, you will be working with the filesystem module to read and write files to the todos JSON file. The JSON file will act as a database for storing, retrieving, and deleting todos. 

Learning Objectives

  • How to require the read and write methods from the Node filesystem promise module
  • Explain the path module’s __dirname argument
  • Understand how to use a JSON file as a database
  • Understand the purpose of using the filesystem promise module
  • Implement functions to read and write to the JSON file
  • Implement handler functions to invoke the read/write functions.
  • Understand the coding practice to create a separation of concerns between handler functions and module methods
  • Explain how to export functions through the use of the Node CommonJS system
  • Understand how to use exported functions in another file

Intended Audience

This course is intended for anyone who wants to learn about Node.js.



Let's Build a Todo CLI With Node.JS - Part Two. In the previous video in this series, I wrote the code to capture the commands from the terminal to execute functions for this Todo CLI application. In this video, I'm going to be using the file system module that is built into Node.js. I'm specifically using the promise space versions of the read and write file methods. Node.js non-blocking IO can cause issues when working with databases by executing code before the database operation is completed. By working with the promise space asynchronous version of the file system module, any code that involves the reading or writing of files will need to complete its execution first before any of the other code can go further. 

I will also need the node path module to create an absolute path to the database. Transition to the lab environment. In the lab environment, I've already created a DB Directory. In this Directory, there are two files. The db.js file, which will have the code to manage the database actions, and the todos.json file, which will act as a database for the todos. To begin, I need to first access the read and write methods from the file system module. I will start with const using the structuring { readfile, writefile} = require node: fs/promises. Now for the path module, const path = requires path. 

Now I will use path  const filePath = path.join( ), and instead of the parentheses, __dirname todos.json. So, what is the purpose of the filePath variable? Well, first, dirname gives the absolute path of the current directory. This absolute path to the directory will be joined with the todo.json file name to create an absolute path to the location of the todos.json file. This will ensure that the todos.json file will be found within the system regardless of which file is accessing it. Now, I'm going to take a moment to discuss how the json file is going to be used as a database. Taking a step back, why do I need the read and write file methods from the file system?

As I mentioned earlier, I am treating this as a database and in order to retrieve the data, I need to use the readfile method to read from this file. And when I wish to save data, I need to use the writefile method to write to this file. And what am I storing in this file? An array of todo objects. Now to the right of the screen is the readfile documentation, which I'm going to use to create a function. Const readTodos = arrow function with no arguments, and I will return the readFile method passing into arguments. The filePath and the utf-8 for encoding. Now I'm going to set up a function to write the todos using the writefile method. That is now shown to the right of the screen. Const writeTodos = arrow function and I'm going to pass in an argument of todos. 

Return writefile with two arguments, the filePath and JSON.stringify (todos). Now I mentioned earlier that I would be saving the todos as an array of todo objects. Because I'm using JSON as a storage from it, I need to convert the objects to JSON strings, which is what I'm doing with the JSON.stringify method. This also has another implication. That means in order to read the data properly, I need to parse it. So, why didn't I do that in the readfile method? Because I will parse the data after the file is read. And I'm going to demonstrate this by creating a new method, getTodos. Const getTodos = arrow function, and inside of the arrow function, I will return readTodos. Because this is promise space, I need to use a then statement to process the promise object. .then (data) arrow function. 

Now before going further, I need to use code to deal with two potential scenarios. First, what if there is no data? As of right now, the todos.json file is blank. So, I will use an if statement. If and for a conditional, I will write exclamation point known as the NOT operator data. I am stating with this code if the data object is undefined, which will happen if the file is blank. Since this is supposed to be an array of todos, I will set this to an empty array of todos. Return data = empty array. And because this is an empty array due to the file being blank, no parsing is necessary. So, what if the file isn't blank, that data does exist? Using an else statement, I will return data = JSON.parse (data), and this will convert the existing JSON objects to regular JavaScript objects. 

So, this may lead to another question. Why did I make this extra function instead of doing this as part of the readTodos function? The main reason is to create a separation of concerns. If I wish to change how the data is processed in the getTodos function, I can do so without affecting the predictable outcome from my readTodos function. This is also beneficial in case I have other functions that rely on the readTodos function by maintaining that predictability. Now this function is completed and ready to test out as part of the command line system. So, in order to do so, I need to export it. Down below, module.exports = an empty object, and inside of this object I will pass getTodos as a property. I am using ES6 syntax here and because the property name matches a function name, the property value, even though not written out, will return the function. 

Now going to the todos.js file, I will import the getTodos function. Const using destructuring {getTodos} = require ./db, which is the directory, /db, which is the file. When using command js, we do not need to put the file extension as part of the file name. Now going down to the switch statement. Since I'm getting all the todos, I'm going to use this command under the view case. I will replace the console.log with getTodos.then. Why .then? Because this is promise space and I want to make sure that the data has been processed before I do anything else, and I will pass through an argument of data using a single line arrow function and console.log the data. This is complete and I will go down to the terminal to test this out, node todo.js view. And in the terminal is an empty array, which is what should be expected because the todos.json file is currently blank.

Now that this is done, the next thing I'm going to do is implement the code that will add a todo to the database. Going back to the db.js file, I'm going to begin by typing const createTodo = arrow function with the argument of todoText. Now I'm going to pause for a moment and talk about the structure of a single todo. A todo is an object and this object will have three properties. First, an ID, and the ID will make it easy to select the todo for deletion and completion. So, the ID needs to be unique. So, I will use a function to generate a random ID number. The second property is the todoText, and this will be coming from the argument of the createTodo function. 

The last property is complete, and this is for completion tracking. All todos when created will have a default completion value of false. Now, I'm going to implement the code to create the todo. I will begin with the ID. I'm not going to code out this function to generate the ID, I'm going to paste it in because this is more for demonstration purposes. And this code will generate a random four-character string which could be all numbers,  or letters, or a mix of both, as shown with the three examples at the bottom of the screen. This isn't a perfect system for uniqueness, but for demonstration purposes, this will work. Now, I'm going to build the structure of a new todo. Const newTodo = an object, and this object will have three properties: ID, todoText, and complete. 

Now for the ID, this is generated by the function above. The todoText is coming from the argument, and complete I'm going to set it with a default value of false. Now that the newTodo has been created, I need to add this to the existing todos array. In order to do this, I need to read the data from the todo.json file. So, I will use the getTodos function that I created earlier in the video, getTodos.then with data as an argument arrow function. And I need to add the newTodo to the data, which represents the todos array, data.push(newTodo). So, now that I have the newTodo added to the existing todos array, what do I need to do next?

I need to write the todos to the todos.json file. As a reference at the bottom of the screen is the writeTodos function that I created earlier in the video. So, I will type writeTodos passing an argument of data. One thing to note here is that the writefile method inside of the writeTodos function overwrites the file and replaces its contents. This is okay in this use case as the newTodo is already part of the existing data that I created when invoking the getTodos function. When the todos are saved to the file, it will include the existing data plus the newTodo. Now this completes the function creation to write a todo. In order to use this function in the todo.js file, I need to export it. Inside of the module.exports, I will write createTodo. So, I'm going to go to the todo.js file in order to import this function and add it to the command line switch statement. 

I will add createTodo through destructuring in the require statement. And now I can use the createTodo function to add a todo. In the switch statement under the add case, I will replace console.log with createTodo. So, now I can test out this function through the add statement by typing in the terminal, node todo.js add "This is my first todo", and I will hit 'Enter'. Pausing for a moment, in hindsight, maybe I should have added a confirmation message to the createTodo function, letting the user know that the createTodo was successful. In this case it's okay because I can use the view command to find out if my todo was written. node todo.js view, and now there is a todo with its four digit ID, the todoText, and the complete status, and the complete status being false. And that's it, thanks for watching, @cloudacademy.


About the Author
Learning Paths

Farish has worked in the EdTech industry for over six years. He is passionate about teaching valuable coding skills to help individuals and enterprises succeed.

Previously, Farish worked at 2U Inc in two concurrent roles. Farish worked as an adjunct instructor for 2U’s full-stack boot camps at UCLA and UCR. Farish also worked as a curriculum engineer for multiple full-stack boot camp programs. As a curriculum engineer, Farish’s role was to create activities, projects, and lesson plans taught in the boot camps used by over 50 University partners. Along with these duties, Farish also created nearly 80 videos for the full-stack blended online program.

Before 2U, Farish worked at Codecademy for over four years, both as a content creator and part of the curriculum experience team.

Farish is an avid powerlifter, sushi lover, and occasional Funko collector.