Your Starting Place: How To Build A CRUD App with Node, Express, and MongoDB
Table of contents
- Prerequisites
- What are CRUD, Node, Express, and MongoDB?
- Project Setup
- Build Out The Backend - READ/GET
- Build Out The Backend - CREATE/POST
- MongoDB Setup & Integration
- Organize The Code
- Back To Building Out The Backend - CREATE/POST
- Build Out The Backend: Displaying Books In The Library - GET/READ
- Build Out The Backend - DELETE
- Build Out The Backend - UPDATE
- Finishing Touches
- Summary
- Review Questions
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:
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.
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.
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.
- This is the modern standard and the one you will use in this project. It uses
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.
- This uses the
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
andres
, respectively, but for clarity’s sake I will use the full wordsrequest
andresponse
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:
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.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.”
Connect to Cluster0
Copy down your username and password when prompted. Then, click “Create Database User.”
Choose a connection method
Select “Drivers”
Make sure Node.js and the latest version are selected
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 connectionStore & 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!!!
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:
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:”
Click “Create Database:”
Enter the name of your database and collection. I’m calling my database “Library” and my collection “bookData.” Then, click “Create:”
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
fileCreate an npm script to make the workflow faster
Move middleware and route handlers into a
createServer()
functionBuild 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:
Title: The name of the book.
Author: Who wrote the book.
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:
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
andbook.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 theread
property of booksEdit
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
objectResponse
objectisRead
, a boolean which will be true if it is being marked read and false if it is being marked unreadThe
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 therequest.body
We will update the
read
property to be theisRead
boolean using the$set
operator. It works like this:$set: {field, value}
. Thefield
is the property we are updating and thevalue
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
ormarkUnread
)bookTitle
, so the server knows which title to delete
Makes a fetch request with a PUT method
Sends
bookTitle
to the server in therequest.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>
elementCall
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 scriptsDynamically 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!
What is CRUD?
What HTTP methods does CRUD correspond with?
What is Node.js?
What are modules?
What is Express?
What is MongoDB?
What is a document in MongoDB?
What is a collection in MongoDB?
What is the relationship between the client, server, and database?
What does a server need?
What do you call the code that runs on a server and defines what happens when a request is made?
How do you initialize a Node project?
What is a package.json file?
How do you start and stop the server in the terminal?
How do you install a package/module in Node?
What are dependencies?
What is the difference between importing modules with CommonJS and ES Modules?
How do you start using Express?
What is a root route?
How do you handle a READ/GET request with Express?
What is an npm script and how do you write one?
How do you get the absolute path to your current directory with Node?
How do you serve an HTML file using Express?
What is the purpose of the action and method attributes in an HTML form?
How do you define what happens when a POST request is made to a specific route in Express?
How do you retrieve form data?
What is Middleware in Express?
How do you tell Node to read an .env file?
How do you add a new document to a collection with MongoDB?
What is EJS used for?
How do you embed a variable in EJS?
How do you embed code in EJS?
How do you delete a document from a collection with MongoDB?