Your Starting Place: 
How To Build A CRUD App with Node, Express, and MongoDB

Your Starting Place: How To Build A CRUD App with Node, Express, and MongoDB

When I started learning backend development, it was daunting. Tools like Node.js, Express.js, and MongoDB felt overwhelming because I didn’t know how to make them work together. My past experiences learning new technologies taught me that the best way to learn was to build--and that's exactly what we're going to do here.

This article is my attempt at creating a comprehensive Starting Place for developers who are coming from frontend JavaScript and want to dive into backend development. By the end of this article, you'll have built a complete CRUD (create, read, update, delete) app using Node.js, Express.js, and MongoDB. You will have the power to take these tools and build anything you want! And you will feel so powerful. Mwahaha.

Through my own journey as a community-taught developer, I've learned that breaking down complex concepts and building hands-on projects is the best way to gain confidence. So, I'm here to guide you step-by-step through the process of building a personal library app, Library Lite. It will have the following functionality:

  • Create: add books to library

  • Read: dynamically display books from the database on the web page

  • Update: mark a book as read or unread

  • Delete: remove books from library

After building Library Lite together, you will know enough that you can extend the web app and make it your own. Perhaps you will include ISBNs or add the ability to edit books. Or perhaps you will take this foundation and create something entirely your own, something that excites you and solves a problem!

I highly recommend that you code along and experiment with the code while you do, as this article was written with that in mind.

At the end of this article, there is a list of review questions that you can use to make sure you understand the concepts that were introduced in this article. You can also use them as the front of your Anki cards!

Bear with me, as this article will be long. Make sure to take breaks and do your pomodoros! I will try to be as clear as possible, but please go easy on yourself! This is a marathon, not a sprint. It’s okay to take your time. If you have any feedback on this article, feel free to contact me using the form on my website.

Here is what Library Lite will look like:

A library demo interface for managing books. There's a section to add books with fields for title and author, and buttons for adding, deleting, and marking books as read or unread. Lists include "Want To Read" and "Read" categories with example books under each. Links for tutorial and source code are visible at the bottom.

DEMO SOURCE CODE

Prerequisites

  • Familiarity with JavaScript and JavaScript objects

  • Having Node.js installed

You can check this by going into your terminal/command line and typing:

# terminal
node -v

If it returns a version number, you already have it installed! I am using v22.12.0 at the time of this article.

What are CRUD, Node, Express, and MongoDB?

CRUD

CRUD stands for Create, Read, Update, and Delete. These are the four basic actions that are at the core of almost every web app you use. In our example from the previous section, when you typed in a URL and hit enter, you made a READ request to the server. Each action of CRUD corresponds with an HTTP request method that is used to actually interact with the server. You will use the HTTP methods when you build your own server. They are included in parenthesis in the following list:

Create (POST)

  • Create something

  • Example: When you post on Bluesky, the client sends a CREATE/POST request to the server with your post.

Read (GET)

  • Get something.

  • Example: When you type in a URL and hit enter or refresh the page, the client makes a READ/GET request to get the website.

Update (PUT)

  • Change something.

  • Example: When you like a post, the client sends an UPDATE/PUT request to update the like count.

Delete (DELETE)

  • Remove something.

  • Example: When you delete a post, the client sends a DELETE request to the server, which removes the post.

Think about some of the web apps you use on a daily basis and try to break down the app functionality into CRUD operations. Soon, you’ll see that you can truly build anything you want using CRUD!

Now that you understand CRUD operations, let's explore the tools we'll use to build our app.

Node.js

Previously, when you used JavaScript for the front-end it was running in the browser. Node.js allows you to run JavaScript outside of the browser on the server-side. It gives us disk and network access for building a server! Node also provides access to modules, which are collections of related reusable pieces of code. Modules allow you to import and use functionality from pre-existing libraries or your own code. Modules are helpful because you don’t need to reinvent the wheel – you can reuse existing solutions.

Express.js

Express is a web framework for working with Node.js. It makes it easy to create servers with Node.js by giving us methods that make it simple to handle CRUD operations. As you start working with Express later in this article, you’ll find that it is readable and intuitive.

MongoDB

MongoDB is a database that stores data in objects. This is a great database to start with because if you know how objects work, you know how MongoDB works.

There are two basic terms you need to be familiar with in MongoDB: documents and collections. Each object (piece of data) is called a document. Collections are a group of documents. For example, in the Library Lite app you will build today, there will be a collection of books and each book will be a document. Each document will include book title, author, and read (a boolean representing whether it was read or not):

{
    "title": "The Stranger",
    "author": "Albert Camus",
    "read": false
}

What is the client-server relationship?

Before we start building, let’s go over the basics of how clients interact with servers and databases. Understanding this relationship is critical for CRUD apps.

Client: A computer/device that makes requests to the server.

  • The client is the device that makes requests to the server. Those requests could be to create, read, update, or delete (CRUD).

  • Example 1: Your personal laptop is a client-side device. When you type a URL into your browser and click enter, you are making a READ/GET request to the server for some files that make up the website you want.

  • Example 2: When you submit a form to join a newsletter, you are making a CREATE/POST request asking the server to add your email to that organization’s email database.

Server: A server is a computer that listens for client requests and responds to them.

  • For a server to work, it needs disk access (to access the data on the SSD, for example) and network access (to hear incoming requests from the client).

  • The code that runs on the server and specifies what to do when a certain request is made is called an Application Programming Interface (API).

  • Example 1: After the client makes a READ/GET request to the server by entering a URL, the server hears that network request, gets the HTML file you requested from its disk (i.e., SSD), and responds with it. The browser is able to read those files and you see the web page on your screen.

  • Example 2: After the client submits a form and makes a CREATE/POST request, the server hears the request, takes the data from the form, adds it to the database, and responds that it was successful.

Project Setup

Phew now that all of that background information is out of the way, let’s get to building! This is the fun part :).

In this section, you will:

  • Initialize the Node project

  • Install Express and MongoDB

  • Set up the server with Express

  • Create the server

First, create a folder/directory for the project called library-lite and add a server.js file. Open the project in your code editor (I use VSCode).

Now let’s initialize our node project!

Initialize Node.js

To initialize Node, we’ll use node package manager (npm). Npm comes with Node.js and is useful because instead of downloading the files for the modules you need, saving them to your project, and manually updating it every time they update the module, you can use npm to install it and update it from the terminal.

Using the terminal, navigate into your project directory using the change directory command: cd <directory>. Once you’re inside the correct directory library-lite, initialize Node:

# terminal
npm init

When you run npm init, your terminal will prompt you through setting up the project. You can hit enter through all of the questions it asks.

A terminal window displaying the creation of a  file using npm. It includes fields like package name, version, description, entry point, author, and license. The details are for a "library-lite" project, with a main file named "server.js" and an undefined test script. The license is ISC, and it confirms if the information is correct.

After you get through the setup, you will notice that a file called package.json appeared! This file is super cool and my favorite thing about Node. It describes everything about the project from the author and description to project dependencies.

Dependencies are which modules are necessary for the project to run. They are called dependencies because the project is dependent on them– it will not function without them. It also contains scripts, which I will get into later in the article.

Here is what my package.json looks like. Alter yours so that “main” is set to “server.js”. You can also include information like your name, app description, and app name:

{
 "name": "library-lite",
 "version": "1.0.0",
 "description": "Personal library app",
 "main": "server.js",
 "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
 "author": "Rai",
 "license": "ISC"
}

In server.js, write a console.log() to ensure that Node is running:

// server.js
console.log('Helloooo');

Let’s run Node for the first time! Do this in the terminal by using the node <path> command:

# terminal
node server.js

If you see Hellooo in the terminal, Node is running! Yay!

To stop the server, hit Control + C in the terminal.

Now that Nose is running, let’s install Express so you can use it to start building the server.

Installing Express

Now, let’s install Express. To install something in Node, use npm install <package-name>:

# terminal
npm install express

Side Note: You can also use npm i <package-name> to install packages.

A new folder called node_modules has been generated inside your project! This is where all of the files for Express and any other project dependencies are stored! If you open the folder, you will see it has a LOOOT of folders & files:

Look at package.json. It automatically updated to include Express as a dependency when you installed it:

{
 "name": "library-lite",
 "version": "1.0.0",
 "description": "Personal library app",
 "main": "server.js",
 "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
 "author": "Rai",
 "license": "ISC",
 "dependencies": {
     "express": "^4.21.2"
   }
}

The coolest part of package.json is that if another developer clones your code, they can type npm install into their terminal and all of the dependencies listed in package.json will automatically install on their machine!

You’ll also see a package-lock.json file– this is created to “lock down” the versions of packages you used so that the same versions are installed across different environments.

Side note: If you have initialized a git repository for this project using git init, create a .gitignore file and put node_modules inside of it so you do not push hundreds of files up to GitHub. If you are not familiar with Git, don’t worry about this right now.

Set Up The Server With Express

The first thing you need to do is import Express into the server.js file so that you can use it.

There are two different systems for importing modules into files: ES Modules and CommonJS. While I won’t dive too deep into the differences, it is important for you to have a basic understanding of both module systems as you may encounter them both.

  1. ES Modules (newer)

    • This is the modern standard and the one you will use in this project. It uses import syntax, which you’ll see in newer projects or when you work with frameworks like React.
  2. CommonJS (older)

    • This uses the require syntax and is still widely used, especially in legacy codebases or projects built before ES Modules became the standard.

Here is how you would import Express using ES Modules and CommonJS, respectively:

// ES Modules Syntax
import express from 'express'; // import syntax

 // CommonJS Syntax
const express = require('express'); // require syntax

Now that you will be able to recognize ES Modules and CommonJS in the wild, let’s move onto starting our app.

First, import Express with ES Modules. Then, create the Express app, which is the core object used to build a server:

// server.js
import express from 'express'; // import with ES Modules
const app = express(); // create instance of Express app

Because we are using ES Modules, we need to go back into package.json and specify that. To do so, we will add a line to specify that the “type” is “module”:

{
 "name": "library-lite",
 "version": "1.0.0",
 "description": "Personal library app",
 "main": "server.js",
 "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
 "author": "Rai",
 "license": "ISC",
 "dependencies": {
     "express": "^4.21.2"
   },
  "type": "module"
}

Create The Server

Let’s create the server! I love Express because it is so readable.

When you want the server to listen on a certain port, all you have to write is app.listen(PORT, callbackFunction)! To make sure our server is running, let’s write a console.log() as the callback function:

// server.js
import express from 'express';
const app = express(); // create instance of Express app

// app listening on PORT 8000
app.listen(8000, () => console.log('Server is running away!!!'))

Now, let’s run our code! Do you remember how to do that in the terminal? Hint: node server.js

If you see Server is running away!!! in the terminal, it is working properly!

If you got this error, you forgot to change the “type” to “module” in your package.json file:

Warning: Module type of file: server.js is not specified and it doesn't parse as CommonJS.
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
To eliminate this warning, add "type": "module" to package.json.
(Use `node --trace-warnings ...` to show where the warning was created)

This error is because Node uses CommonJS by default, so we need to specify that we are using ES Modules. We don’t want to create performance overhead– this means Node is using extra resources which can slow things down. So make sure you add that line to package.json.

Congratulations, you’ve successfully built your first server! Now let’s work on our first CRUD operation – READ!

Build Out The Backend - READ/GET

In this section, you will:

  • Handle a GET request in Express

  • Learn about the request and response objects

  • Serve a webpage to the browser

First, let’s see what happens when we make a READ/GET request to our server. Make sure your server is running. In your browser, go to http://localhost:8000/, since the server is listening on PORT 8000.

  • The page will say Cannot GET / This is because we made a GET request from the browser/client, but there isn’t any code on the server that defines how to handle a GET request at the root route (basically the default route, just the domain name and a forward slash / at the end).

  • You will still see Server is running away!!! in your console, because the server is running, it just isn’t sending any response to the client!

Now, let’s specify what should happen on the server when the client navigates to our page. In more technical words, we are going to write the API on our server to handle when the client makes a READ/GET request to the root route.

Handle A GET Request In Express

To define the route for a READ/GET request using Express, there is a built-in HTTP method:

app.get(endpoint, handler)

Endpoint: what comes after the domain name. For example, the endpoint for https://raisadorzback.netlify.app/ is / You can also use a full path instead of just an endpoint if needed.

The handler is a callback function that will run when the route is matched. It expects two parameters: request and response.

  • The request object represents the HTTP request

  • The response object represents the HTTP response

  • These are usually denoted by req and res, respectively, but for clarity’s sake I will use the full words request and response in this article & code.

Let’s use app.get() to define our GET route:

// server.js
import express from 'express';
const app = express();

// handle GET request
app.get('/', function(request, response) {
   // when GET request is made, send string response
   response.send('If you can see this, the server sent a response!');
});

app.listen(8000, () => console.log('Server is running away!!!'));

This code specifies that when a GET request is made at the endpoint / (the root route), the server will send a response of If you can see this, the server sent a response! Do you see what I mean about Express being readable? When you want to send a response, all you need to remember is response.send(). When you want to handle a GET request to your app, you just have to remember app.get().

Now, restart the server and go to http://localhost:8000/ to make sure it is working.

You should see If you can see this, the server sent a response! on the screen.

To make this code a bit more concise, I am going to change the handler function into an arrow function:

// server.js
app.get('/', (request, response) => {
   response.send('If you can see this, the server sent a response!');
});

Another milestone unlocked! You have sent your first response from the server to the client!

Now, instead of responding with a string, let’s move onto responding with an HTML file.

Serve Your First Webpage

Let’s serve an actual HTML file to the client!

First, create a new file called index.html. Inside index.html, add a basic boilerplate HTML file and an <h1>:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
   <title>Document</title>
</head>
<body>
   <h1>Welcome To Library Lite!</h1>
</body>
</html>

Now we are going to update the server to send the index.html file as a response instead of sending a plain string. To do this, we’ll use response.sendFile(filePath).

Node version 20.11.0 introduced a handy feature to use with ES Modules that gives us the absolute path to the current directory: import.meta.dirname. Let’s use that in response.sendFile():

// server.js
import express from 'express';
const app = express();
const dirname = import.meta.dirname; // get path to directory

app.get('/', (request, response) => {
    // use response.sendFile() to serve HTML
    response.sendFile(dirname + '/index.html'); // concatenate index.html to the directory name to find the file
});
app.listen(8000, () => console.log('Server is running away!!!'));

Switching from response.send() to response.sendFile() allows you to serve full-fledged HTML pages, which is how you build web apps! You are one step closer to building a real interface for your app. If you’re curious, go ahead and console.log(dirname) to see its contents.

Restart the server and refresh the page. You should see the <h1> from your HTML file displayed on the page!

Now that you know how to serve an HTML file, let’s make it interactive and build a <form> for users to add books to the library!

Build Out The Backend - CREATE/POST

In this section, you will:

  • Create a form for users to add books

  • Define the backend route for the POST request

  • Retrieve data from a form

To bring this library app to life, we will build a simple <form> that lets users add new books—this will be the first step toward handling CREATE requests.

Create <form> to Add Books

A form needs two attributes:

  1. The action attribute specifies where to send the request. In this case, the <form> will send the request to the endpoint /addBook. You can name your endpoint whatever you’d like, I chose /addBook for clarity.

  2. The method attribute tells the browser what kind of request to send. In this case it will be a POST request since submitting the form creates something new.

In index.html, add a <form> and a submit <button> below the <h1>. Within each <input>, add a placeholder, type, name, id, and required boolean:

<!-- index.html -->
<!-- form -->
<h2>Add Books</h2>
<form action="/addBook" method="POST" class="flex flex-col">
   <!-- Inputs Container -->
   <div>
      <!-- Title Input Container -->
      <div>
         <!-- text input label -->
         <label for="title">Title</label>
         <!-- text input for title -->
         <input
             placeholder="The Stranger"
             type="text"
             name="title"
             id="title"
             required >
      </div>
       <!-- Author Input Container -->
      <div>
         <!-- author input label -->
         <label for="author">Author</label>
         <!-- text input for author -->
         <input
            placeholder="Albert Camus"
            type="text"
            name="author"
            id="author"
            required >
      </div>
   </div>
   <!-- Submit button to add book -->
   <button type="submit">Add Book</button>
</form>

Side Note: The <div>s around each input and label are being used as containers to group together the labels with their inputs. This is going to make it easier for me later to style the app using CSS Flexbox. We are not going to cover CSS nor flexbox in this article, so it is up to you if you want to organize your form into <div>s in this way.

The name attribute on each input acts like a label for each field. When the form is submitted, the data from each input will be packed into an object. The key will be the name attribute we specified and the value will be whatever was written in the input. If there is no name, the input data won’t be sent to the server. If a user inputs The Stranger by Albert Camus, an object like this will be sent to the server in the request.body:

{
    'title': 'The Stranger',
    'author': 'Albert Camus'
}

To test this, restart the server (reminder: Control + C to stop your server, node server.js to restart it) and refresh the page so that you can see the changes on http://localhost:8000/. You should see the form on the page!

Now we need to actually build the part of the server that the client is sending the data to.

Define Backend Route for the POST Request

While GET requests are used to read data, POST requests are used to send data from the client (frontend) to the server (backend). Do you remember how we used app.get(endpoint, handler) to handle the GET request earlier? Can you guess how we might handle a POST request in Express? That’s right: app.post(endpoint, handler). Let’s use /addBook as the endpoint and make the handler function a simple console.log() to confirm that our server is successfully handling the request:

// server.js
// define POST route
app.post('/addBook', (request, response) => {
   console.log('Is this thing on?');
});

Restart the server, refresh your page, and test this out! Type in a book and click submit. If everything is working correctly, you should see Is this thing on? in your terminal:

You will notice that when you submit a book, the page has a loading spinner. This is because the browser expects a response from the server after it makes a request. The browser will indefinitely wait until it receives a response from the server. To prevent the infinite loading spinner, we will have the server respond telling the client to reload the homepage after the form is submitted using response.redirect('/'). With that addition, the POST route should look like this:

// server.js
// POST route
app.post('/addBook', (request, response) => {
   console.log('Is this thing on?');
   // redirect to home page
   response.redirect('/');
});

Save the file, restart the server, refresh the page, and submit a book again. The loading circle should be gone!

You have successfully set up your form and handled your first POST request! Next, we will work on retrieving the book data that was sent to the server.

Retrieve Data From A Form

To retrieve the data submitted through the form, Express needs help from middleware.

Middleware

Middleware are functions that act as helpers. They process requests and responses before they reach your route handlers. We’ll use middleware to parse the incoming form data and add it to the request object so we can easily access it in the request.body.

Express provides a built-in middleware: express.urlencoded(). Add this middleware to your app with app.use() (this is how you use any middleware) and make sure it comes above your route handlers in server.js:

// server.js
import express from 'express';
const app = express();
const dirname = import.meta.dirname;

/* MIDDLEWARE */
app.use(express.urlencoded({ extended: true }))

/* ROUTE HANDLERS */
app.get('/', (request, response) => {
   response.sendFile(`${dirname}/index.html`);
});

app.post('/addBook', (request, response) => {
   console.log('Is this thing on?');
   response.redirect('/');
});

app.listen(8000, () => console.log('Server is running away!!!'));

Now that the middleware is set up, log the request.body to see it in action:

// server.js 
app.post('/addBook', (request, response) => {
   // LOG THE REQUEST.BODY
   console.log(request.body);
   response.redirect('/');
});

Restart the server and submit a book. In the terminal, you should see the request.body. It should look like this:

Want to explore more? Try logging the entire request object. If you scroll through it, you’ll find a key called body containing the parsed form data. Now, comment out the middleware and log request again. Notice how the body is missing? This demonstrates how essential middleware is for accessing form data.

Now that you can get the data out of the form, the next step is to save it in a database! Before we can do that, we need to take a detour to setting up MongoDB and connecting to the database.

MongoDB Setup & Integration

MongoDB is the database where you will store the book data. It's beginner-friendly and works seamlessly with JavaScript since documents in MongoDB are stored as objects. For this app, we will be using MongoDB Atlas specifically so that our database can live in the cloud rather than on our local machine.

In this section:

  • Create and set up MongoDB Atlas account

  • Connect the app to MongoDB

  • Set up the database in MongoDB Atlas

Create & Set Up MongoDB Atlas Account

First, create an account

  • Sign up (you can use your Google account for convenience)

  • Follow the prompts and select the free tier when asked to “Deploy your cluster.” Then click “Create Deployment.”

    "Deploy your cluster" page for MongoDB. Three cluster options are displayed: M10, Serverless, and M0. The M0 option is highlighted with the text "Click the free option!" indicating it is free. Configuration and setup options are shown below.

Connect to Cluster0

  • Copy down your username and password when prompted. Then, click “Create Database User.”

    MongoDB Atlas setup page, showing steps to connect to a cluster. It includes instructions to add an IP address and create a database user. A red arrow and box highlight the "Create Database User" button, with the instruction "Click this button!"

Choose a connection method

MongoDB Atlas interface showing steps for connecting to a cluster. It advises adding a connection IP address and creating a database user. There's a button labeled "Choose a connection method" with an arrow and "Click here!" label.

Select “Drivers”

MongoDB connection interface, highlighting "Connect to your application" using "Drivers" for accessing Atlas data.

Make sure Node.js and the latest version are selected

Screenshot of instructions for connecting to MongoDB Cluster0 using the Node.js driver. Steps include selecting the driver version, installing the driver via the command line, and adding a connection string to application code. A partially redacted connection string is shown, with placeholders for sensitive information.

Save the connection string on this page, we’ll update it shortly.

Now that you have an account and a connection string, it’s time to connect our app to MongoDB.

Connect To MongoDB

Steps to connect to MongoDB:

  • Install MongoDB in your project

  • Import MongoDB into server.js

  • Write function to connect to the database

  • Test the connection

Install MongoDB

First, install the MongoDB package in your project directory with npm install mongodb.

Import MongoDB into server.js

To interact with MongoDB, we need to import MongoClient from MongoDB. MongoClient is a tool MongoDB provides to interact with your database. Think of it as a "bridge" between your Node.js app and MongoDB. At the top of server.js with your other import statement:

// server.js
import { MongoClient } from 'mongodb'; // import MongoClient with ES Modules

Write function to connect to the database

Let’s write a function called connectToDatabase(). Its purpose is to–you guessed it–connect to our MongoDB database. To do this, we will:

  • Use MongoClient.connect(connectionString) to establish a connection

  • Store & use the connection string you got from MongoDB

  • Log a success message if the connection is successful or an error if it is not.

// server.js

// create uri variable for connection string
const uri = 'mongodb+srv://<username>:<password>@cluster0.u0qtw.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0'; // replace the username and password with yours

// asynchronous function to connect to database
async function connectToDatabase() {
 try {
   // connect using connection string uri
   await MongoClient.connect(uri);
   console.log('Connected to DB');
   // catch errors
 } catch(err) {
   console.error(err);
 };
};

At the bottom of your server.js file, call connectToDatabase() to initiate the connection to MongoDB:

// server.js
connectToDatabase();
app.listen(8000, () => console.log('Server is running away!!!'));

Phew, that was a lot! But we got through it. Moment of truth. Let’s test it out.

Test The Database Connection

Restart the server. If you see the following in your console, you are connected to the database!!!

Terminal that says "Server is running away!!! Connected to DB"

If you encounter an error, double-check your connection string. If your password includes special characters, ensure it’s encoded with encodeURIComponent(). If it still doesn’t work, in MongoDB Atlas, click “Connect” and whitelist your IP address:

MongoDB Atlas dashboard showing the "Overview" page. The "Connect" button is highlighted in a red box.

Congratulations, you have successfully connected to MongoDB! Give yourself a pat on the back, I know that was a lot to get through.

You’re almost done setting up MongoDB Atlas. Before you can save the form data into MongoDB, you need to create a specific database for this library app.

Set Up Your Database in MongoDB Atlas

Go back to MongoDB Atlas. You should see a screen like this. Click “Browse Collections:”

MongoDB Atlas interface, showing an overview of a cluster named "Cluster0" with options to "Connect," "Edit configuration," "Browse collections," and "View monitoring." "Browse collections" is highlighted in a red box

Click “Create Database:”

MongoDB Atlas interface. The "Create Database" button is highlighted.

Enter the name of your database and collection. I’m calling my database “Library” and my collection “bookData.” Then, click “Create:”

MongoDB Atlas interface showing a "Create Database" popup. The database name is set to "Library" and the collection name is "bookData." The "Create" button is highlighted in red.

From here, you will be able to see the data inside your collection:

Update connectToDatabase()

Now that we have our Library database and bookData collection set up in MongoDB Atlas, let’s update connectToDatabase() to access them both! The function will also return the bookData collection so we can work with it elsewhere (i.e., to add books submitted through the form to the database/books collection):

// server.js
async function connectToDatabase() {
 try {
   const client = await MongoClient.connect(uri);
   console.log('Connected to DB');
   // assign Library database to db variable
   const db = client.db('Library');
   // assign bookData collection to books variable
   const books = db.collection('bookData');
   // return books so we can work with it elsewhere
   return books;
 } catch(err) {
   console.error(err);
 };
};

Before building out the rest of the CREATE/POST functionality, let’s organize the code so that it is cleaner, readable, and more secure.

Organize The Code

The code is going to get more complex as you continue to build out the rest of the CRUD functionality. Before adding on that complexity, it is important that our code is organized and secure. In this section, you will:

  • Store sensitive information such as the database connection string in an .env file

  • Create an npm script to make the workflow faster

  • Move middleware and route handlers into a createServer() function

  • Build a main() function to run the app

Store Sensitive Information In .env Files

To keep your app secure, you’ll use an .env file to store sensitive information like the database connection string as well as app-specific configurations like your server’s PORT. Using .env files protects sensitive data and makes it easier to adjust configuration settings for different environments. Built-in .env file support was introduced in Node v20.6.0.

Create .env file

Create an .env file in the project folder with touch .env.

Add your database string and PORT to it. Replace <username> and <password> with your actual database username password.

# .env
DB_STRING = mongodb+srv://<username>:<password>@cluster0.u0qtw.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
PORT = 8000

Tip: If you’re using Git, add .env to .gitignore so you don’t push your secrets to GitHub for all to see. If you’re not, don’t worry about this step.

Update server.js with .env variables

Replace the PORT and uri with the environment variables using process.env.variableName:

// server.js
const PORT = process.env.PORT;
const uri = process.env.DB_STRING;

Alter the part of the code that listens on a port 8000 to use the PORT variable:

// server.js
app.listen(PORT, () => console.log('Server is running away!!!'));

Tell Node to read our .env file

So far, you have run the server with the command node server.js. From now on, you need to tell Node to load the .env file when starting the app by using this command: node --env-file=.env server.js.

Now, test it to make sure the app is still working as expected. Restart your server with the new command node --env-file=.env server.js. It should work exactly the same as it did before we made this change.

This command is very lengthy and you won’t want to write it out every single time you make a change. To avoid repeatedly typing it, we are going to create a custom command by writing an npm script.

Create an npm script

Npm scripts are custom commands defined in package.json that automate tasks—in this case, running the project—without typing them out each time.

Go into package.json and look for where it says “scripts”. You should see one script that says "test": "echo \"Error: no test specified\" && exit 1". This was automatically generated when we initialized our Node project and we can ignore it for now.

Underneath the test script, let’s write a script called dev (because we will use it during development) to run our new command:

{
 "name": "library-lite",
 "version": "1.0.0",
 "description": "Personal library app",
 "main": "server.js",
 "type": "module",
 "scripts": {
   "test": "echo \"Error: no test specified\" && exit 1",,
   "dev": "node --env-file=.env server.js"
 },
 "author": "Rai",
 "license": "ISC",
 "dependencies": {
   "express": "^4.21.2",
   "mongodb": "^6.12.0"
 }
}

Test it out! Restart the server using the new npm script. To run custom npm scripts, use: npm run <scriptName>:

# terminal
npm run dev

Now you can use this much shorter command to run the app! If your script isn’t working, make sure you’ve saved your package.json file, used the correct command, refreshed the page, and run the command from the project directory.

Even though this command is concise, your workflow is still slow because you have to manually restart the server every time you change the code. There is something we can add to our npm script so that the server automatically watches for changes.

Make Your Workflow Faster

So far every time you’ve updated the code, you had to manually stop running the code and restart it, now with npm run dev. It is good to get into the practice of manually restarting your server, but if you are making a lot of changes very quickly it is faster to use the built-in --watch flag that was introduced with Node v18.11.0. If you’re using an older version of Node, you’ll need to update it to use this feature. With the --watch flag, Node will automatically restart the server whenever it detects a change in your code. You will still have to refresh the page to see the changes, but it will save a lot of time.

In package.json, alter the dev script to include the --watch flag:

"scripts": {
   "dev": "node --watch --env-file=.env server.js"
 }

Let’s test it out! Restart the server with npm run dev, go into your code and temporarily change the <h1> in your index.ejs. Refresh the page (but don’t restart the server). The page should be updated with your new <h1>!

Now that your workflow is faster and your sensitive data is protected, let’s organize the code inserver.js so everything looks smooth before we finish the CREATE functionality and add more complex code.

createServer() function

We are going to write a createServer() function. The middleware and route handlers will live inside of it. We will also pass the books collection into createServer() so that we interact with it within the server:

// server.js
function createServer(books) {
 /* ===========
 MIDDLEWARE
 =========== */
 app.use(express.urlencoded({ extended: true }))

 /* ===========
 ROUTE HANDLERS
 =========== */
 // handle GET request
 app.get('/', (request, response) => {
     response.sendFile(`${dirname}/index.html`);
 });

 // handle POST request
 app.post('/addBook', (request, response) => {
     console.log(request.body);
     response.redirect('/');
 });
// run server on PORT
 app.listen(PORT, () => console.log('Server is running away!!!'));
};

Now that the server is within the createServer() function, let’s write a main() function that will call the functions to run the app.

Create main() function

Let’s create a main() function that will run the app. It will call the functions createDatabase() and createServer():

// server.js
/* ===========
MAIN FUNCTION TO START APPLICATION
=========== */
async function main() {
 try {
   // call connectToDatabase() and assign return value (books collection) to books variable
   const books = await connectToDatabase();
   // call createServer() and pass in books
   createServer(books);
   // error handling
 } catch(err) {
   console.error(`Failed to start application: ${err}`);
 };
};

// RUN APP
main();

With all of the changes you’ve made in this section, here is how server.js should look as a whole:

// server.js
/* ===========
IMPORTS
=========== */
import express from 'express'; // import express with ES Modules
import { MongoClient } from 'mongodb'; // import MongoClient with ES Modules

/* ===========
VARIABLES
=========== */
const app = express(); // create instance of Express app
const dirname = import.meta.dirname; // get dirname
const PORT = process.env.PORT; // get PORT from .env file
const uri = process.env.DB_STRING; // get DB_STRING from .env file

/* ===========
CONNECT TO DATABASE
=========== */
async function connectToDatabase() {
 try {
   const client = await MongoClient.connect(uri);
   console.log('Connected to DB');
   const db = client.db('Library');
   const books = db.collection('bookData');
   return books;
 } catch(err) {
   console.error(err);
 };
};

/* ===========
SERVER AND API
=========== */
function createServer(books) {
 /* ===========
 MIDDLEWARE
 =========== */
 app.use(express.urlencoded({ extended: true }))

 /* ===========
 ROUTE HANDLERS
 =========== */
 // handle GET request
 app.get('/', (request, response) => {
     response.sendFile(`${dirname}/index.html`);
 });

 // handle POST request
 app.post('/addBook', (request, response) => {
     console.log(request.body);
     response.redirect('/');
 });

 app.listen(PORT, () => console.log('Server is running away!!!'));
};

/* ===========
MAIN FUNCTION TO START APPLICATION
=========== */
async function main() {
 try {
   const books = await connectToDatabase();
   createServer(books);
 } catch(err) {
   console.error(`Failed to start application: ${err}`);
 };
};

// RUN APP
main();

Refresh the page (since your server is automatically restarting now) and test your connection to make sure it’s still working! The app should work exactly the same way it did before you made these changes.

Now that your code looks beautiful, your workflow is super speedy, and your sensitive data is protected, let’s finally get back to saving books from our form into our database.

Back To Building Out The Backend - CREATE/POST

Let’s review what you did earlier when you started working on the CREATE/POST part of the app. You:

  • Built a form for users to add books

  • Defined the POST route

  • Used middleware to retrieve the data from the <form>

The last piece to make this work is to actually save the data you have retrieved in the database.

When we left off, all the POST route handler did was log the request.body and tell the client to refresh the page with response.redirect(‘/’). First, make the handler function asynchronous and add a try…catch statement. We’ll put the existing code within the try block and do error handling in the catch block:

// server.js

// add async keyword to callback function
app.post('/addBook', async (request, response) => {
// try…catch block
   try {
      console.log(request.body);
      response.redirect('/');
   } catch(err) {
     // handle errors
     console.error(`Could not add book: ${err}`);
   };
 });

As discussed earlier, in MongoDB each piece of data is called a document. Instead of just logging the result, the function will insert a document (the book that was submitted) into the database. For example, each book added to the database will be a document like this:

{
    "title": "The Stranger",
    "author": "Albert Camus",
    "read": false
}

To store complete information about a book, we’ll include three properties:

  1. Title: The name of the book.

  2. Author: Who wrote the book.

  3. Read: A boolean value (true or false) indicating whether the book has been read. This will be helpful later on when we will mark books as read or unread.

Like Express, MongoDB is quite readable! Want to insert one document into the collection? Use MongoDB’s db.collection.insertOne() method:

// server.js
app.post('/addBook', async (request, response) => {
   try {
    // insert book into collection
     const result = await books.insertOne({
       title: request.body.title, // get title and author from the request.body
       author: request.body.author,
       read: false // add a read property of false by default
      }
    );
     // log result
     console.log(result);
     // refresh page afterwards
     response.redirect('/');

     // error handling
   } catch(err) {
     console.error(`Could not add book: ${err}`);
   };
 });

In the above code, insertOne() is called on the books collection. A book object is passed in as the document. The title and author are accessed from the request.body and the read property is manually added and set to false whenever a new book is added.

Refresh your page and submit a book to the form to test this. In your console, you should see the result, which will be an object like this:

If you see an object like that in the console, the book was successfully added to the database! Now, head over to your MongoDB Atlas dashboard, click on the bookData collection, and check if the new book appears there. It should look like this:

You now have CREATE functionality in your app! I know it took a lot to get here, but doesn’t it feel great? This was probably the most difficult part of the entire app so it’s all downhill from here (in a good way lol)!

Now that our app saves books to the database, let’s move on to the next step: retrieving and displaying the books dynamically in your library!

Build Out The Backend: Displaying Books In The Library - GET/READ

So users can now add books to the library so we need to display those books on the page! To do this:

  • Retrieve books from database

  • Dynamically render the books on the page

Retrieve Books From the Database

To retrieve all the documents (books) stored in MongoDB, we’ll use the db.collection.find() method. This method doesn’t return the documents directly. Instead, it returns something called a cursor which is like a reference to all of the documents inside the collection. You can read more cursors in MongoDB here.

Let’s modify our GET route handler to use the db.collection.find() method and log the cursor so you can see what it looks like:

// server.js
app.get('/', (request, response) => {
  // find documents in collection
  const cursor = books.find()
  // log cursor
  console.log(cursor);
  response.sendFile(`${dirname}/index.html`);
});

The cursor object is very large and unreadable. However, it contains a reference to all of the documents! To extract the documents and turn them into an array, use the toArray() method. We will also turn the handler/callback function into an asynchronous function:

// server.js
// make route handler asynchronous
 app.get('/', async (request, response) => {
   // find books, turn cursor to an array of book documents
   const bookList = await books.find().toArray();
   // log book list
   console.log(bookList);
   response.sendFile(`${dirname}/index.html`);
 });

Refresh the page. You should see an array of the books you added in your console:

A code snippet displaying an array of books with unique IDs, titles, authors, and their read status (true or false)

You have successfully retrieved the documents from the database! Great job! The next step is plugging these books into the HTML so they can be displayed on the page! To do this, we will use a templating language called Embedded JavaScript (EJS).

Dynamically Render Books On the Page

EJS is a templating language. An easy way to think about it is as HTML with JavaScript sprinkled (or embedded) inside of it. We will be able to pass the book data into the EJS and use it there! This is very useful because we can loop through the book data and display each book that way.

First, install EJS with npm install ejs. Then we need to let Express know that EJS is being used as the templating engine. To do so, write this line of code at the very top of the createServer() function before the middleware:

// server.js
app.set('view engine', 'ejs');

Now create a folder called views and an index.ejs file inside of it. Copy and paste everything from index.html into index.ejs:

<!-- index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
   <title>Document</title>
</head>
<body>
   <h1>Welcome To Library Lite!</h1>
   <!-- Add Books Section -->
   <section>
       <h2>Add Books</h2>
       <form action="/addBook" method="POST" class="flex flex-col">
           <div>
               <!-- Title Input -->
               <div>
                   <label for="title">Title</label>
                   <input
                       placeholder="The Stranger"
                       type="text"
                       name="title"
                       id="title"
                       required >
                </div>
                <!-- Author Input -->
                <div>
                    <label for="author">Author</label>
                    <input
                        placeholder="Albert Camus"
                        type="text"
                        name="author"
                        id="author"
                        required >
                </div>
           </div>
           <button type="submit">Add Book</button>
       </form>
   </section>
</body>
</html>

You can also delete the index.html file, we won’t need it anymore.

Now that we are rendering EJS instead of HTML, we have to update the GET route to reflect that.

Rendering EJS

When we were rendering HTML, we used response.sendFile(). In this case, we will use Express’s response.render(view, locals) method to render our EJS.

  • View is the name of the file to be rendered

  • Locals is the data being passed into the file (in this case, that data will be bookList, the array of books).

Let’s modify the GET route handler to render the EJS:

// server.js
app.get('/', async (request, response) => {
   try {
     const bookList = await books.find().toArray();
     console.log(bookList);
     // render EJS, pass in bookList data
     response.render('index.ejs', { bookInfo: bookList })
   } catch(err) {
     console.error(`Error retrieving list of books ${err}`);
   };
 });

The view is index.ejs and for the locals, we are passing in an object with the bookList. I called it bookInfo for clarity. You can call it anything you want, but whatever name you choose is how you have to reference it in your EJS.

Refresh your page. It should look exactly the same as it did when your app was rendering index.html.

Loop Through Books In EJS

Earlier, you passed the bookList to EJS as bookInfo. With EJS, we can loop through bookInfo and render each book as an <li>. There are some special tags in EJS that we need in order to do this:

  • This tag is used to embed the values of variables like book.title and book.author inside the HTML: <%= variableGoesHere %>

  • This tag is used for JavaScript code (in this case, the for loop) that controls flow but doesn’t output anything: <% codeGoesHere %>

Create a new section for displaying the books, create a <ul>, and loop through the bookList, which will be known within index.ejs as bookInfo. For each book, create an <li> with a <span> for the text. Then we’ll plug the book’s title and author into the span with <%= book.title %> by <%= book.author %>:

<!-- index.ejs -->
<section>
   <h2>Your Books</h2> 
   <!-- unordered list -->
   <ul>
      <!-- loop through bookInfo -->
      <% for(const book of bookInfo) { %>
         <!-- for each book, create an li -->
         <li>
            <span>
               <!-- plug in the book title and author -->
               <%= book.title %> by <%= book.author %>
            </span>
         </li>
      <!-- end of for loop -->
      <% } %>
   </ul>
</section>

Now refresh your page in the browser. You should see all of your books in a list! How cool is that?

Congratulations, your app now dynamically displays data!!

Let’s take the app’s functionality a step further by adding the ability to delete books from the app!

Build Out The Backend - DELETE

Library Lite now has CREATE and READ functionality, so let’s add DELETE functionality! Users will be able to delete books by clicking a button.

To accomplish this:

  • Create a DELETE route

  • Add a delete button to each book in the list

  • Add client-side JavaScript in order to:

    • Listen for a click on the delete button

    • Send DELETE request to the server

Creating A DELETE Route

First, we’ll define the DELETE route in server.js. Express has yet another super readable method to listen for DELETE requests – app.delete(endpoint, handler). Let’s make the endpoint /deleteBook for clairty:

// server.js
// handle DELETE request
app.delete('/deleteBook', (request, response) => {
    // delete logic will go here
});

Let’s make the handler function asynchronous. Use a try…catch block for error handling:

// server.js
// handle DELETE request
app.delete('/deleteBook', async (request, response) => {
  try {
    // delete logic will go here
  } catch(err) {
     console.error(`Failed to delete book: ${err}`);
  };
});

To actually delete the book from the database, use MongoDB’s db.collection.deleteOne(). We will delete the book based on its title:

// server.js
// handle DELETE request
app.delete('/deleteBook', async (request, response) => {
  try {
    // delete book by title
    const result = await books.deleteOne({title: request.body.title});
    // respond to client that it was successful
    response.json('Book deleted successfully');
    // error handling
  } catch(err) {
     console.error(`Failed to delete book: ${err}`);
  };
});

In the above code, we are accessing the title from the request.body. However, Express doesn’t parse the body of incoming requests so request.body will be undefined. We need request.body to include the actual book title though! To achieve this, we need to use a piece of middleware that will parse incoming JSON data and make it accessible as request.body. Add this at the top of the createServer() function next to your other middleware:

// server.js
app.use(express.json());

Side Note: You can optimize the DELETE route by deleting books based on their unique ID (the _id property automatically generated for each document in MongoDB) instead of their title. This would make it more reliable, especially if there are duplicate titles in the database. Take a crack at it after you finish working through this article. You may need to use some outside resources to figure out how to do this.

Now let’s add a delete button to each book!

Add Delete Button To Each Book

In index.ejs, add a delete button to the <li> and give it a class of delete:

<!-- index.ejs -->
<ul>
   <% for(const book of bookList) { %>
      <li>
         <span>
            <%= book.title %> by <%= book.author %>
         </span>
        <!-- delete button -->
        <button class="delete">Delete</button>
      </li>
   <% } %>
</ul>

Refresh your page. It should look like this:

Now that there is a delete button for each book, we need to add event listeners to the buttons. We will do this in the client-side JavaScript.

Add Client-Side JavaScript

In order to add event listeners to the delete buttons, we need to actually have a file for our client-side JavaScript! So create a public folder and put a main.js file inside of it.

In Express, we can put static files (i.e., client-side JavaScript, CSS, images, fonts) in a public folder and it will automatically serve those files without us explicitly having to write a route for them. We need to use a piece of middleware to tell Express to do this. Add this with your other middleware in createServer():

// server.js
app.use(express.static('public'));

Now we need to link the main.js with index.js. Add a script tag with the path to main.js as the src:

<!-- index.ejs -->
<body>
   <!-- all your code -->
   <!-- link to main.js file -->
   <script src="/main.js"></script>
</body>

Now that we’ve got main.js linked to the ejs, let’s finally work on main.js!

Add Event Listeners

Let’s finally add event listeners to our delete buttons!

First, create an array of all of the delete buttons:

// main.js
// select all of the buttons with a class of ‘delete’ from the DOM and create an array from them
const deleteButtons = Array.from(document.querySelectorAll('.delete'));

Next, loop through the array and add an event listener to each button:

// main.js
// create array of delete buttons
const deleteButtons = Array.from(document.querySelectorAll('.delete'));
// loop through deleteButtons array
deleteButtons.forEach(button => {
   // For each button, add an event listener that listens for a click. On click, run the deleteBook() function
   button.addEventListener('click', deleteBook);
});

Let’s write the deleteBook() function! First, we will put a console.log() in the function just to ensure it is working:

// main.js
// create array of delete buttons
const deleteButtons = Array.from(document.querySelectorAll('.delete'));

// add event listeners to delete buttons
deleteButtons.forEach(button => {
   button.addEventListener('click', deleteBook);
});

// function that will be called when user clicks on button
function deleteBook() {
   console.log('This function will delete the book');
};

Test this by refreshing the page, opening the console in your browser (hint: use your browser’s dev tools), and clicking one of the delete buttons. When you do so, your browser console should output This function will delete the book. If you see that in your browser console, then the event listeners are successfully attached to the delete buttons!

Send DELETE Request to Server

Let’s work on the deleteBook() function in main.js so that it triggers a DELETE request to be sent to the server.

First, let’s get the book title based on which button was clicked:

// main.js
function deleteBook() {
   let bookTitle = this.parentNode.textContent;
};

this refers to the button that was pressed. The parentNode of the button is the <li>. The textContent is the text inside of it. For example, bookTitle will grab “The Stranger by Albert Camus:” Side Note: You likely won’t use parentNode as a way to grab things in production, but it will work as a starting place for your first couple CRUD apps.

If you console.log() the bookTitle, you’ll notice the book has a lot of space around it. Let’s clean bookTitle up by trimming the whitespace, splitting the string, and isolating just the title itself:

// main.js
function deleteBook() {
   let bookTitle = this.parentNode.textContent;
   // manipulate string to access only the book title from textContent
   bookTitle = bookTitle.trim().split(' by ')[0];
};

Now for actually sending the DELETE request with the title to the server! We will do this by making a fetch request. Since fetch() is asynchronous, we will make the function async and add a try…catch block. In the fetch request, we need to specify some things about the request we are sending:

  • HTTP method – in this case, DELETE

  • Headers – specify what kind of data we are sending. In this case, JSON

  • Body – this will be request.body that is sent to the server. We will put the bookTitle in the body

// main.js
async function deleteBook() {
   try {
       let bookTitle = this.parentNode.textContent;
       bookTitle = bookTitle.trim().split(' by ')[0];

       // fetch request to ‘/deleteBook’ route
       const result = await fetch('/deleteBook', {
           // specify the delete HTTP method
           method: 'delete',
           // specify we are sending JSON
           headers: {'Content-Type': 'application/json'},
           // send the title to the backend in the request.body and convert to JSON
           body: JSON.stringify({
               title: bookTitle
           })
       })
       // await result of request, convert from JSON object to JavaScript. Will be "Book deleted successfully" if successful
       const data = await result.json();
       console.log(data)
       // refresh page
       location.reload();
   } catch(err) {
       console.error(`Error deleting book: ${err}`);
   };
};

Test this out by refreshing the page and opening the browser console. Delete one of your books! In the console, you should see Book deleted successfully although it will go away very fast since we are refreshing the page. The book itself should also disappear from your screen.

Now your app has CREATE, READ, and DELETE functionality!!! 75% of the way there! The final step is adding the UPDATE feature so users can mark books as "read" or "unread." Let’s tackle that next!

Build Out The Backend - UPDATE

To help users track their reading progress, we’ll introduce two new features:

  • A "Mark Read" button to move books to a "Read" list

  • A "Mark Unread" button to move books back to "Want To Read” list

These updates give users flexibility to manage their library as they read and reread books (or if they accidentally click “Mark Read”).

Here’s the plan:

  • Define the PUT route in server.js to update the read property of books

  • Edit index.ejs

    • Create two sections: "Want To Read" and "Read."

    • Add "Mark Read" and "Mark Unread" buttons

  • Attach event listeners to the buttons in main.js

  • Send UPDATE/PUT request to the server

Define PUT Route

We are going to define two PUT routes in server.js. One will update the read property on books from false to true. The other will update read from true to false. In Express, we do this with–I bet you can guess this one–app.put(endpoint, handler). There will be two PUT routes. One that marks books as read and one that marks books as unread:

// main.js
// handle UPDATE request - mark read
app.put('/markRead', (request, response) => {
   /* ... */ 
});

// handle UPDATE request - mark unread
app.put('/markUnread', (request, response) => {
   /* ... */ 
});

Now we need to write our handler functions to update the book’s read property in the database. If we were to write the handler functions directly in the PUT routes as we have for our other routes, the code would be almost identical. Aka we would have WET (write everything twice) code. To keep our code DRY, we’ll write a reusable function called markBook() that we will call within each PUT route.

The markBook() function:

  • Takes the following parameters:

    • Request object

    • Response object

    • isRead, a boolean which will be true if it is being marked read and false if it is being marked unread

    • The books collection, which we will use to update the book

  • Uses MongoDB’s db.collection.updateOne(filter, update) method to update the read property depending on whether the user is marking a book as read or unread, respectively.

    • The filter we will use is the book title. That way, we update whichever book has the title that was sent in the request.body

    • We will update the read property to be the isRead boolean using the $set operator. It works like this: $set: {field, value}. The field is the property we are updating and the value is what we are updating it with.

// server.js
// asynchronous function
async function markBook (request, response, isRead, books) {
 try {
   const result = await books.updateOne({
     // update the book based on its title
     title: request.body.title
   },{
     // set the read property to isRead (either true or false)
     $set: {
       read: isRead
     }
   });
   // send response that it was marked as read or unread (depending on isRead)
   response.json(`Marked ${isRead ? 'Read' : 'Unread'}`)
 } catch(err) {
   // log error 
   console.error(`Failed to mark book`);
 }
};

Now let’s update the PUT routes to be asynchronous and await the result of markBook():

// server.js
// handle UPDATE request - mark read
app.put('/markRead', async (request, response) => {
  await markBook(request, response, true, books)
});

// handle UPDATE request - mark unread
app.put('/markUnread', async (request, response) => {
  await markBook(request, response, false, books)
});

Your backend can now update the read property for any book in the database! Let’s connect this to the frontend. First, we’ve got to make two sections: “Read” and “Want to Read.”

Edit index.ejs

We need to display books in two separate lists based on their read property. If read is false, the book should be displayed “Want to Read.” If read is true, books should be displayed in “Read.” We will also add “Mark Read” and “Mark Unread” buttons to the books depending on which list they are in.

In index.ejs, let’s add:

  • An <h2> of “Want To Read”

  • An if statement within the for loop to check each book’s read property before rendering the <li>

  • A “Mark Read” button with a class of mark-read

<!-- index.ejs -->
<section>
    <!-- h2 for Want To Read -->
    <h2>Want To Read</h2>
    <ul>
       <% for(const book of bookInfo) {
           <!-- if the book's read property is false display it in this list -->
           if(book.read === false) { %>
               <li>
                   <span>
                       <%= book.title %> by <%= book.author %>
                   </span>
                   <button class="delete">Delete</button>
                   <!-- Mark Read button on items in "Want To Read" -->
                   <button class="mark-read">Mark Read</button>
               </li>
           <% }
       } %>
    </ul>
</section>

Then, copy and paste that code to make another section, change the <h2> to “Read,” the if statement to check if book.read is true instead of false, and a “Mark Unread” button with a mark-unread class:

<!-- index.ejs -->
<!-- Section of both lists -->
<section>
       <!-- Want To Read Section -->
       <section>
           <h2>Want To Read</h2>
           <ul>
               <% for(const book of bookInfo) {
                   if(book.read === false) { %>
                       <li>
                           <span>
                               <%= book.title %> by <%= book.author %>
                           </span>
                           <button class="delete">Delete</button>
                           <button class="mark-read">Mark Read</button>
                       </li>
                   <% }
               } %>
           </ul>
       </section>
       <!-- Read Section -->
       <section>
           <h2>Read</h2>
           <ul">
               <% for(const book of bookInfo) {
                   <!-- if the book has been read display it in Read list -->
                   if(book.read === true) { %>
                       <li>
                           <span>
                               <%= book.title %> by <%= book.author %>
                           </span>
                           <button class="delete">Delete</button>
                           <!-- Mark Unread Button -->
                           <button class="mark-unread">Mark Unread</button>
                       </li>
                   <% }
               } %>
           </ul>
       </section>
   </section>

Challenge: Can you refactor this code to make it more DRY (Do not repeat yourself) and efficient? Avoid looping through bookInfo twice by creating reusable logic to handle both lists.

Now the lists are ready for interactivity. Let’s attach event listeners to the buttons and connect them to the backend!

Add Event Listeners to Mark Read and Unread Buttons

Let’s make our buttons usable by adding event listeners to the new buttons and sending FETCH requests to the backend.

First, grab the Mark Read and Mark Unread buttons from the DOM by their classes and turn them into arrays:

// main.js
const markReadButtons = Array.from(document.querySelectorAll('.mark-read'));
const markUnreadButtons = Array.from(document.querySelectorAll('.mark-unread'));

Loop through the arrays of buttons and attach event listeners. When clicked, these buttons will call either markRead() or markUnread():

// main.js
const markReadButtons = Array.from(document.querySelectorAll('.mark-read'));
const markUnreadButtons = Array.from(document.querySelectorAll('.mark-unread'));

// add event listeners to Mark Read buttons
markReadButtons.forEach(button => {
   button.addEventListener('click', markRead);
});

// add event listeners to Mark Unread buttons
markUnreadButtons.forEach(button => {
   button.addEventListener('click', markUnread);
});

To keep things DRY, we’ll create a generic function mark() which sends a PUT request to the backend. Then, markRead() and markUnread() will each call it and pass in their respective endpoints. The mark() function:

  • Accepts the following parameters:

    • endpoint (/markRead or markUnread)

    • bookTitle, so the server knows which title to delete

  • Makes a fetch request with a PUT method

  • Sends bookTitle to the server in the request.body

// main.js
async function mark(endpoint, bookTitle) {
   try {
       const result = await fetch(endpoint, {
           // put method
           method: 'put',
           headers: {'Content-Type': 'application/json'},
           // send bookTitle in request.body
           body: JSON.stringify({
               title: bookTitle
           })
       });
       const data = await result.json();
       // refresh page
       location.reload();
   } catch(err) {
       console.error(`Error marking book: ${err}`);
   };
};

Now let’s write markRead() and markUnread(). These functions will:

  • Isolate the book title from the <li> element

  • Call mark() with the appropriate endpoint and book title

// main.js
async function markRead() {
   let bookTitle = this.parentNode.textContent;
   bookTitle = bookTitle.trim().split(' by ')[0];
   await mark('/markRead', bookTitle);
};

async function markUnread() {
   let bookTitle = this.parentNode.textContent;
   bookTitle = bookTitle.trim().split(' by ')[0];
   await mark('/markUnread', bookTitle);
};

Clicking the buttons should now update the book’s read property in the database. Let’s test it out!

  • Save all the changes

  • Refresh the page

  • Click “Mark Read” on one of your existing books. It should move to “Want To Read!”

  • Click “Mark Unread.” It should move back to “Read!”

Here’s how your page should look:

Congratulations! YOU HAVE OFFICIALLY BUILT A COMPLETE CRUD APP. All it needs are some finishing touches/beautification and it’ll be done!

Finishing Touches

Your CRUD app is fully functional! The last step is adding some styling to our app so that it looks nice! Add a style.css file to your public folder and link the file to your index.ejs by putting the following code into the <head> of your ejs file:

<!-- index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
   <title>Document</title>
   <link rel="stylesheet" href="/style.css">
</head>

I won’t be covering the actual styling with CSS in this article, but I sincerely hope that you make this app your own, in terms of both style and function. Now that you have the CRUD functionality, you can build anything you want!

Summary

Phewwww we covered a lot here!! Together, we:

  • Went through what CRUD, Node.js, Express.js, and MongoDB are and how they’re used

  • Implemented full-stack Create, Read, Update, and Delete operations

  • Set up MongoDB Atlas

  • Worked with .env files and npm scripts

  • Dynamically rendered HTML using a templating language

You should be very proud of yourself for getting through this and building a full-stack CRUD app. I encourage you to modify the code you wrote for this web app to create a completely different full-stack web app and work up to making it portfolio-ready! Time to do a happy dance – you’ve come a long way!

Review Questions

The following questions will help you review everything you’ve learned in this article. If you use Anki (which I highly recommend) or another flashcard app, feel free to use these as the front of your flashcards and write your own answers for the back!

  1. What is CRUD?

  2. What HTTP methods does CRUD correspond with?

  3. What is Node.js?

  4. What are modules?

  5. What is Express?

  6. What is MongoDB?

  7. What is a document in MongoDB?

  8. What is a collection in MongoDB?

  9. What is the relationship between the client, server, and database?

  10. What does a server need?

  11. What do you call the code that runs on a server and defines what happens when a request is made?

  12. How do you initialize a Node project?

  13. What is a package.json file?

  14. How do you start and stop the server in the terminal?

  15. How do you install a package/module in Node?

  16. What are dependencies?

  17. What is the difference between importing modules with CommonJS and ES Modules?

  18. How do you start using Express?

  19. What is a root route?

  20. How do you handle a READ/GET request with Express?

  21. What is an npm script and how do you write one?

  22. How do you get the absolute path to your current directory with Node?

  23. How do you serve an HTML file using Express?

  24. What is the purpose of the action and method attributes in an HTML form?

  25. How do you define what happens when a POST request is made to a specific route in Express?

  26. How do you retrieve form data?

  27. What is Middleware in Express?

  28. How do you tell Node to read an .env file?

  29. How do you add a new document to a collection with MongoDB?

  30. What is EJS used for?

  31. How do you embed a variable in EJS?

  32. How do you embed code in EJS?

  33. How do you delete a document from a collection with MongoDB?