Building a Simple CRUD app with Node, Express, and MongoDB

Zell Liew 🤗 - Apr 23 '20 - - Dev Community

I finally understood how to work with Node, Express, and MongoDB. I want to write a comprehensive tutorial so you won't have to go through the same headache I went through.

CRUD, Express and MongoDB

CRUD, Express and MongoDB are big words for a person who has never touched any server-side programming in their life. Let's quickly introduce what they are before we diving into the tutorial.

Express is a framework for building web applications on top of Node.js. It simplifies the server creation process that is already available in Node. In case you were wondering, Node allows you to use JavaScript as your server-side language.

MongoDB is a database. This is the place where you store information for your websites (or applications).

CRUD is an acronym for Create, Read, Update and Delete. It is a set of operations we get servers to execute (POST, GET, PUT and DELETE requests respectively). This is what each operation does:

  • Create (POST) - Make something
  • Read (GET)- Get something
  • Update (PUT) - Change something
  • Delete (DELETE)- Remove something

POST, GET, PUT, and DELETE requests let us construct Rest APIs.

If we put CRUD, Express and MongoDB together into a single diagram, this is what it would look like:

Does CRUD, Express and MongoDB makes more sense to you now?

Great. Let's move on.

We'll build a simple application together

Let's build a simple application that lets you track a list of quotes from Star Wars Characters. Here's what it looks like:

Add CSS to make the app look better.

Free free to check out the demo before continuing with this tutorial.

:::note
This article is LONG! Remember to grab the source code by leaving your name and email address in this form. I'll also send you this article in PDF so you can read it at your leisure.
:::

By the way, I'm not going to focus on the styles since we're focusing on learning Crud, Express, and MongoDB in this tutorial.

Prerequisites

You'll need two things to get started with this tutorial:

  1. You are not afraid of typing commands into a Command Line. If you're afraid, use this article to get over your fear.
  2. You need to have Node installed.

To check if you have Node installed, open up your Command Line and run the following code:

$ node -v
Enter fullscreen mode Exit fullscreen mode

Node version

You should get a version number if you have Node installed. If you don't, you can install Node either by downloading the installer from Node's website or downloading it through package managers like Homebrew (Mac) and Chocolatey (Windows).

Getting started

Start by creating a folder for this project. Feel free to call it anything you want. After you've created the folder, navigate into it with the Terminal and run npm init.

npm init creates a package.json file which helps you manage dependencies (which we will install as we go through the tutorial).

$ npm init
Enter fullscreen mode Exit fullscreen mode

npm init.

Just hit enter through everything that appears. I'll talk about the ones you need to know as we go along.

Running Node for the first time in your life

The simplest way to use node is to run the node command, and specify a path to a file. Let's create a file called server.js to run node with.

touch server.js
Enter fullscreen mode Exit fullscreen mode

Next, put this a console.log statement into server.js. This lets us know whether Node is running properly.

// server.js
console.log("May Node be with you");
Enter fullscreen mode Exit fullscreen mode

Now, run node server.js in your command line and you should see this:

Logged May Node be with you.

Great. Node works. The next step is to learn to use Express.

Using Express

First, we have to install Express. We can do this by running the npm install command. (npm is installed with Node, which is why you use commands like npm init and npm install).

Run npm install express --save command in your command line.

:::note
The --save flag saves express as a dependency in package.json. It's important to know these dependencies because npm can retrieve dependencies with another npm install command when you need it later.
:::

npm install express --save
Enter fullscreen mode Exit fullscreen mode

Saved express as a dependency

Next, we use express in server.js by requiring it.

const express = require("express");
const app = express();
Enter fullscreen mode Exit fullscreen mode

We need to create a server that browsers can connect to. We do this by using the Express's listen method.

app.listen(3000, function () {
  console.log("listening on 3000");
});
Enter fullscreen mode Exit fullscreen mode

Now, run node server.js and navigate to localhost:3000 on your browser. You should see a message that says cannot get /.

Cannot get /.

That's a good sign. It means we can now communicate to our express server through the browser. This is where we begin CRUD operations.

CRUD - READ

Browsers perform the READ operation when you visit a website. Under the hood, they send a GET request to the server to perform this READ operation.

You see cannot get / because our server sent nothing back to the browser.

In Express, we handle a GET request with the get method:

app.get(endpoint, callback);
Enter fullscreen mode Exit fullscreen mode

endpoint is the requested endpoint. It's the value that comes after your domain name. Here are some examples:

  • When you visit localhost:3000, you're actually visiting localhost:3000/. In this case, browsers requested for /.
  • You're reading this article on https://zellwk.com/blog/crud-express-mongodb/. The domain name is zellwk.com. The requested endpoint is anything that comes after zellwk.com (which is /blog/crud-express-mongodb).

callback tells the server what to do when the requested endpoint matches the endpoint stated. It takes two arguments: A request object and a response object.

// We normally abbreviate `request` to `req` and `response` to `res`.
app.get("/", function (req, res) {
  // do something here
});
Enter fullscreen mode Exit fullscreen mode

For now, let's write Hello World back to the browser. We do so by using a send method that comes with the response object:

app.get("/", function (req, res) {
  res.send("Hello World");
});
Enter fullscreen mode Exit fullscreen mode

I'm going to start writing in ES6 code and show you how to convert to ES6 along the way as well. First off, I'm replacing function() with an ES6 arrow function. The below code is the same as the above code:

app.get("/", (req, res) => {
  res.send("Hello World");
});
Enter fullscreen mode Exit fullscreen mode

Now, restart your server by doing the following:

  1. Stop the current server by hitting CTRL + C in the command line.
  2. Run node server.js again.

Then, navigate to localhost:3000 on your browser. You should be able to see a string that says "Hello World".

Great.

Next, let's change server.js so we serve up an index.html page back to the browser. To do this, we use the sendFile method that's provided by the res object.

app.get("/", (req, res) => {
  res.sendFile(__dirname + "/index.html");
  // Note: __dirname is directory current directory you're in. Try logging it and see what you get!
  // Mine was '/Users/zellwk/Projects/demo-repos/crud-express-mongo' for this app.
});
Enter fullscreen mode Exit fullscreen mode

In the sendFile method above, we told Express to serve an index.html file that can be found in the root of your project folder. We don't have that file yet. Let's make it now.

touch index.html
Enter fullscreen mode Exit fullscreen mode

Let's put some text in our index.html file as well:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>MY APP</title>
  </head>
  <body>
    <h1>May Node and Express be with you.</h1>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Restart your server and refresh your browser. You should be able to see your HTML file now.

The HTML file.

This is how Express handles a GET request (READ operation) in a nutshell.

At this point, you probably have realized that you need to restart your server whenever you make a change to server.js. This is process is incredibly tedious, so let's take a quick detour and streamline it by using a tool called nodemon.

Enter Nodemon

Nodemon restarts the server automatically when you save a file that's used by the server.js. We can install Nodemon with the following command:

$ npm install nodemon --save-dev
Enter fullscreen mode Exit fullscreen mode

:::note
We use a --save-dev flag here because we only use Nodemon when we are developing stuff. We won't use Nodemon on an actual server. --save-dev here adds Nodeman as a devDependency in the package.json file.
:::

Saved Nodemon as a dev dependency.

Nodemod behaves like Node. So you can run nodemon server.js and you'd expect to see the same thing. Unfortunately, this only works if you've installed nodemon globally with the -g flag (and we didn't do this).

We have other ways to run Nodemon. For example, you can execute Nodemon directly from the node_modules folder. This is super unweildy, but it works:

./node_modules/.bin/nodemon server.js
Enter fullscreen mode Exit fullscreen mode

We can make things simpler by adding script key in the package.json file. This lets us run nodemon server.js without the ./node_modules... preamble.

{
  // ...
  "scripts": {
    "dev": "nodemon server.js"
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Now, you can run npm run dev to trigger nodemon server.js.

Uses npm run dev to run nodemon server.js

Back to the main topic. We're going to cover the CREATE operation next.

CRUD - CREATE

Browsers can only perform a CREATE operation if they send POST request to the server. This POST request can be triggered through JavaScript or through a <form> element.

Let's figure out how to use a <form> element to create new entries for this Star Wars quote application for now. We'll examine how to send requests via JavaScript later.

To send a POST request through a <form>, you need to add the <form> element to your index.html file.

You need three things on this form element:

  1. An action attribute
  2. A method attribute
  3. name attributes on each <input> elements within the form
<form action="/quotes" method="POST">
  <input type="text" placeholder="name" name="name" />
  <input type="text" placeholder="quote" name="quote" />
  <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

The method tells browsers what kind of request to send. In this case, we use POST because we're sending a POST request.

The action attribute tells the browser where to send the POST request. In this case, we're send the POST request to /quotes.

We can handle this POST request with a post method in server.js. The path path should be the value you placed in the action attribute.

app.post("/quotes", (req, res) => {
  console.log("Hellooooooooooooooooo!");
});
Enter fullscreen mode Exit fullscreen mode

Restart your server (hopefully you've set up Nodemon so it restarts automatically) and refresh your browser. Then, enter something into the <form> element and submit the form. Next, look at your command line. You should see Hellooooooooooooooooo! in your command line.

Logs helloooo in the command line.

Great, we know that Express is handling the form for us right now. The next question is, how do we get the input values with Express?

Turns out, Express doesn't handle reading data from the <form> element on it's own. We have to add another package called body-parser to gain this functionality.

npm install body-parser --save
Enter fullscreen mode Exit fullscreen mode

Body-parser is a middleware. They help to tidy up the request object before we use them. Express lets us use middleware with the use method.

const express = require("express");
const bodyParser = require("body-parser");
const app = express();

// Make sure you place body-parser before your CRUD handlers!
app.use(bodyParser.urlencoded({ extended: true }));

// All your handlers here...
app.get("/", (req, res) => {
  /*...*/
});
app.post("/quotes", (req, res) => {
  /*...*/
});
Enter fullscreen mode Exit fullscreen mode

The urlencoded method within body-parser tells body-parser to extract data from the <form> element and add them to the body property in the request object.

You should be able to see values from the <form> element inside req.body now. Try doing a console.log and see what it is!

app.post("/quotes", (req, res) => {
  console.log(req.body);
});
Enter fullscreen mode Exit fullscreen mode

You should see an object similar to the following:

Logs the request body.

Hmmm.

Master Yoda has spoken! Let's make sure we remember Yoda's words. It's important. We want to be able to retrieve it the next time we load our index page.

Enter the database, MongoDB.

MongoDB

MongoDB is a database. We can store information into this database to remember Yoda's words. Then, we can retrieve this information and display to people who view our app.

:::note
I normally use Mongoose (which is a framework for MongoDB) when I use MongoDB. I'll teach you how to use basic MongoDB in this article. If you want to learn Mongoose, consider reading my article on Mongoose.
:::

First, we need to install MongoDB via npm.

npm install mongodb --save
Enter fullscreen mode Exit fullscreen mode

Once installed, we can connect to MongoDB through the MongoClient's connect method as shown in the code below:

const MongoClient = require("mongodb").MongoClient;
Enter fullscreen mode Exit fullscreen mode
MongoClient.connect("mongodb-connection-string", (err, client) => {
  // ... do something here
});
Enter fullscreen mode Exit fullscreen mode

The next part is to get the correct link to our database. Most people store their databases on cloud services like MongoDB Atlas. We're going to do same as well. (It's free).

:::note
You can also create a database on your computer for development work. Read "How to setup a local MongoDB Connection" for instructions.
:::

Setting up MongoDB Atlas

Go ahead and create an account on MongoDB Atlas. Once you're done, you need to create an "Organization". It's sort of like a company name. You can name it anything you want. (You can change it later).

Creates an Atlas Organization

You also need to select a cloud service. Go ahead with MongoDB Atlas in this case.

Selects a cloud service.

Next, you need to set permissions for users. MongoDB Atlas will automatically fill up your current email address as the user. So just continue to the next step.

You should end up with a screen that looks like this:

Atlas project screen.

Next, you need to create a Database in MongoDB Atlas. There are several steps to do this.

First, you need to create a new Project. You can do this by going under "Context" in the top left hand menu. Click the Dropdown. Then, select New Project.

Creating a new project.

Next, you will need to name your project. Call it anything you want. I'm going to call this star-wars.

Naming the project.

Then, you will need to add members. Again, you're already added so go ahead and click "Create Project" to move on.

You should end up with a screen that says Create a Cluster.

Create Cluster Screen.

Click on "Build a Cluster". You should see this screen:

Selecting a cluster

Select the free cluster (left option) and continue. You should now see a screen to configure a cluster. Scroll down. Make sure you see these two things:

  1. Cluster Tier is M0 Sandbox
  2. Monthly Estimate is FREE
Configured the cluster.

Click on Create cluster next. You should see "Your cluster is being created".

Creating cluster.

You have to wait for approximately 5 minutes for the cluster creation. When the cluster is ready, you'll see this:

Cluster ready

Now, we need to connect our Star Wars app with this cluster.

Connecting to MongoDB Atlas

Click on the Connect button.

Connect button.

A modal should pop up.

Connection modal.

You need to whitelist your IP address before you can connect to your cluster. This is a security feature built into MongoDB Atlas. Go ahead and click "Add your Current IP Address".

Adds IP address.

Next, you need to create a MongoDB user. This username and password is different from the one you used to login to MongoDB Atlas. This username and password is used ONLY for the database.

Make sure you remember MongoDB user and password. We'll use it to connect to the database.

MongoDB User.

Next, click on choose your connection method. Select "Connect to your application" and copy the connection string.

Choosing a connection method. Copy the connection string.

The connection string should look something like this:

"mongodb+srv://<username>:<password>@<clustername>-rmp3c.mongodb.net/test?retryWrites=true&w=majority";
Enter fullscreen mode Exit fullscreen mode

You need to replace 2 things here:

  1. Replace <username> with your Database username
  2. Replace <password> with the Database user's password

:::note
The test in the connection string points to a test database. You would need to replace test with the name of your database if you use Mongoose. You can leave it as test if you use MongoClient like what we're doing in this tutorial.
:::

Put this connection string inside the MongoClient.connect.

MongoClient.connect(connectionString, (err, client) => {
  // ... do something here
}))
Enter fullscreen mode Exit fullscreen mode

We know we've connected to the database if there are no errors. Let's create console.log statement that says "Connected to database". This will help us know we've connected to the database when we restart the server.

MongoClient.connect(connectionString, (err, client) => {
  if (err) return console.error(err);
  console.log("Connected to Database");
});
Enter fullscreen mode Exit fullscreen mode

You should see something like this:

Connected to database.

You can remove the deprecation warning by adding the option into MongoClient.connect

MongoClient.connect(
  connectionString,
  {
    useUnifiedTopology: true,
  },
  (err, client) => {
    if (err) return console.error(err);
    console.log("Connected to Database");
  }
);
Enter fullscreen mode Exit fullscreen mode

Connection without deprecation warning.

MongoDB supports promises. If you want to use promises instead of callbacks, you can write MongoClient.connect like this. It behaves exactly like the code above.

MongoClient.connect(connectionString, { useUnifiedTopology: true })
  .then((client) => {
    console.log("Connected to Database");
  })
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

:::note
Read this article if you want to learn about promises in JavaScript.
:::

Changing the Database

We need to change the database from test to something else. You can name it anything you want. I chose name my new database star-wars-quotes because it helps me remember what I'm building.

MongoClient.connect(connectionString, { useUnifiedTopology: true }).then(
  (client) => {
    console.log("Connected to Database");
    const db = client.db("star-wars-quotes");
  }
);
Enter fullscreen mode Exit fullscreen mode

MongoDB and Server

We need the db variable from the connection to to access MongoDB. This means we need to put our express request handlers into the MongoClient's then call.

MongoClient.connect(/* ... */)
  .then((client) => {
    // ...
    const db = client.db("star-wars-quotes");
    app.use(/* ... */);
    app.get(/* ... */);
    app.post(/* ... */);
    app.listen(/* ... */);
  })
  .catch(console.error);
Enter fullscreen mode Exit fullscreen mode

We can finally store Yoda's quote into the database now!

CRUD - CREATE (continued)

We need to create a collection before we can store items into a database. Here's a simple analogy to help you clear up the terms in MongoDB:

  • Imagine a Database is a Room.
  • A Room contains boxes (collections).

Like Databases, you can name collections anything you want. In this case, let's store quotes into a quotes collection. We use db.collection to specify the collection.

MongoClient.connect(/* ... */).then((client) => {
  // ...
  const db = client.db("star-wars-quotes");
  const quotesCollection = db.collection("quotes");

  // ...
});
Enter fullscreen mode Exit fullscreen mode

We can use the insertOne method to add items into a MongoDB collection.

app.post("/quotes", (req, res) => {
  quotesCollection
    .insertOne(req.body)
    .then((result) => {
      console.log(result);
    })
    .catch((error) => console.error(error));
});
Enter fullscreen mode Exit fullscreen mode

Try submitting the <form> from the browser. You should see a big scary looking result in the Terminal.

Post result.

If you see this, congratulations! You've successfully add the quote into the database.

You can check the items inside the database by going to "Collections" in MongoDB Atlas.

Collections in MongoDB Atlas.

You should see a document in your database. (Each database entry is called a document).

Documents in MongoDB Atlas.

If you go back to the Browser, you'll see it's still trying to load something.

Browser still trying to load a page.

This happens because the browser expects something back from the server.

In this case, we don't need to send the browser information. Let's ask the browser to redirect back to / instead. We do this with res.redirect.

app.post("/quotes", (req, res) => {
  quotesCollection
    .insertOne(req.body)
    .then((result) => {
      res.redirect("/");
    })
    .catch((error) => console.error(error));
});
Enter fullscreen mode Exit fullscreen mode

Redirected. Browser is no longer waiting to load something.

Yay!

Since we have some quotes in the collection, let's show them to our user when they land on the page!

Showing quotes to users (READ operation)

We need to do two things to show quotes from MongoDB Atlas to our users.

  1. Get quotes from MongoDB Atlas.
  2. Rendering the quotes in HTML with a template engine

Let's go one step at a time.

Getting quotes from MongoDB

We can get quotes we stored in MongoDB with the find method. This method from mLab by using the find method that's available in the collection method.

app.get("/", (req, res) => {
  const cursor = db.collection("quotes").find();
  console.log(cursor);
  // ...
});
Enter fullscreen mode Exit fullscreen mode

The find method returns a cursor which won't make sense if you tried logging it.

Cursor object from MongoDB

But this cursor object contains all quotes from our database! It has a bunch of method that lets us get our data. For example, we can use toArray to convert the data into an array.

app.get("/", (req, res) => {
  db.collection("quotes")
    .find()
    .toArray()
    .then((results) => {
      console.log(results);
    })
    .catch((error) => console.error(error));
  // ...
});
Enter fullscreen mode Exit fullscreen mode

Great! We see the quotes we added! (You see so many of the same quotes because I added them all when writing this tutorial 😆).

Next we want to generate a HTML that contains all our quotes.

Rendering the HTML

We cannot serve up the index.html file and expect quotes to magically appear because there's no way to add dynamic content to a HTML file.

What we can do, instead, is to use a template engine to generate the HTML. Popular template engines include Pug, Embedded JavaScript, and Nunjucks.

:::note
I've wrote extensively about the how and why of template engines in a separate post. You might want to check it out if you have no idea what template engines are.

I use Nunjucks as my template engine of choice. Feel free to check out the post to find out why.
:::

For this tutorial, we will use Embedded JavaScript (EJS) as our template engine because it's the easiest to start with. You'll find it familiar from the get-go since you'll be writing HTML and JavaScript.

Using EJS

First, we need to install EJS.

npm install ejs --save
Enter fullscreen mode Exit fullscreen mode

Next, we need to set view engine to ejs. This tells Express we're using EJS as the template engine. You can need to place it before any app.use, app.get or app.post methods.

app.set("view engine", "ejs");

// Middlewares and other routes here...
Enter fullscreen mode Exit fullscreen mode

We can now generate HTML that contains the quotes. This process is called rendering the HTML.

We will use the render method built into Express's response. It needs to follow the following syntax:

res.render(view, locals);
Enter fullscreen mode Exit fullscreen mode
  • view is the name of the file we're rendering. This file must be placed inside a views folder.
  • localsis the data passed into the file.

Let's create a view. We'll make an index.ejs file inside the views folder.

mkdir views
touch views/index.ejs
Enter fullscreen mode Exit fullscreen mode

We'll copy/paste everything from index.html into index.ejs.

<!-- index.ejs -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Star Wars Quote App</title>
  </head>

  <body>
    <h1>May Node and Express be with you.</h1>

    <form action="/quotes" method="POST">
      <input type="text" placeholder="name" name="name" />
      <input type="text" placeholder="quote" name="quote" />
      <button type="submit">Submit</button>
    </form>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Next, we'll use res.render to render this index.ejs file.

app.get("/", (req, res) => {
  db.collection("quotes").find().toArray().then(/* ... */).catch(/* ... */);
  res.render("index.ejs", {});
});
Enter fullscreen mode Exit fullscreen mode

If you refresh the page, you should still see the same thing. Nothing should change, nothing should break.

Renders an ejs file.

Let's put the quotes into index.ejs. To do this, we need to pass the quotes into the render method.

app.get("/", (req, res) => {
  db.collection("quotes")
    .find()
    .toArray()
    .then((results) => {
      res.render("index.ejs", { quotes: results });
    })
    .catch(/* ... */);
});
Enter fullscreen mode Exit fullscreen mode

In index.ejs, we can use place variables between <%= and %> tags. Let's try putting quotes into the HTML:

<!-- In index.ejs -->
<body>
  <h1>...</h1>
  <form>...</form>
  <%= quotes %>
</body>
Enter fullscreen mode Exit fullscreen mode

You should see this:

Rendered quotes in EJS.

We see lots of [object Object] because each quote inside results is a JavaScript object. ejs cannot convert that object into HTML automatically.

We need to loop through the quotes. We can do this with a for loop. In EJS, we write a for loop like how we write a JavaScript for loop. The only difference is we need to put the for loop statements between <% and %>.

<h2>Quotes</h2>

<ul class="quotes">
  <!-- Loop through quotes -->
  <% for(var i = 0; i < quotes.length; i++) {%>
  <li class="quote">
    <!-- Output name from the iterated quote object -->
    <span><%= quotes[i].name %></span>:
    <!-- Output quote from the iterated quote object -->
    <span><%= quotes[i].quote %></span>
  </li>
  <% } %>
</ul>
Enter fullscreen mode Exit fullscreen mode

Rendered quotes with EJS.

CRUD - UPDATE

We use the UPDATE operation when we want to change something. It can be triggered with a PUT request. Like POST, PUT can be triggered either through JavaScript or through a <form> element.

Let's switch things up and use JavaScript since you already know how to use <form> elements.

For this update operation, we will create a button that replaces the first quote by Yoda to something written by Darth Vadar.

To do this, we need to add a button into the index.ejs file:

<div>
  <h2>Darth Vadar invades!</h2>
  <p>
    Replace first Yoda's quote with a quote written by Darth Vadar
  </p>
  <button id="update-button">Replace Yoda's quote</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Add update section to HTML.

We will also create an external JavaScript file to execute a PUT request. According to Express conventions, this JavaScript is kept in a folder called public

$ mkdir public
$ touch public/main.js
Enter fullscreen mode Exit fullscreen mode

Then, we have to tell Express to make this public folder accessible to the public by using a built-in middleware called express.static

app.use(express.static("public"));
Enter fullscreen mode Exit fullscreen mode

We now can add the main.js file to the index.ejs file:

<body>
  <!-- ... -->
  <script src="/main.js"></script>
</body>
Enter fullscreen mode Exit fullscreen mode

We will send a PUT request when the button gets clicked. This means we need to listen to a click event.

Next, we're going to send the PUT request when the button is clicked:

// main.js
const update = document.querySelector("#update-button");

update.addEventListener("click", (_) => {
  // Send PUT Request here
});
Enter fullscreen mode Exit fullscreen mode

Sending a PUT Request

The easiest way to trigger a PUT request in modern browsers is to use the Fetch API.

Fetch has the following syntax:

fetch(endpoint, options);
Enter fullscreen mode Exit fullscreen mode

In this case, let's say we want to send the request to /quotes. We'll set endpoint to /quotes.

update.addEventListener("click", (_) => {
  fetch("/quotes", {
    /* ... */
  });
});
Enter fullscreen mode Exit fullscreen mode

We need to send a PUT request this time. We can do this by setting Fetch's method to put.

update.addEventListener("click", (_) => {
  fetch("/quotes", {
    method: "put",
  });
});
Enter fullscreen mode Exit fullscreen mode

Modern applications send JSON data to servers. They also receive JSON data back to servers. JSON stands for JavaScript Object Notation. They're like JavaScript objects, but each property and value are written between two quotation marks.

Here's an example of JavaScript data:

const data = {
  name: "Darth Vadar",
  quote: "I find your lack of faith disturbing.",
};
Enter fullscreen mode Exit fullscreen mode

And what its JSON counterpart looks like. (Notice how everything is wrapped between two ").

{
  "name": "Darth Vadar",
  "quote": "I find your lack of faith disturbing."
}
Enter fullscreen mode Exit fullscreen mode

We need to tell the server we're sending JSON data by setting the Content-Type headers to application/json.

update.addEventListener("click", (_) => {
  fetch("/quotes", {
    method: "put",
    headers: { "Content-Type": "application/json" },
  });
});
Enter fullscreen mode Exit fullscreen mode

Next, we need to convert the data we send into JSON. We can do this with JSON.stringify. This data is passed via the body property.

update.addEventListener("click", (_) => {
  fetch("/quotes", {
    method: "put",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      name: "Darth Vadar",
      quote: "I find your lack of faith disturbing.",
    }),
  });
});
Enter fullscreen mode Exit fullscreen mode

Accepting the PUT request

Our server doesn't accept JSON data yet. We can teach it to read JSON by adding the body-parser's json middleware.

app.use(bodyParser.json());
Enter fullscreen mode Exit fullscreen mode

Next, we can handle the PUT request with a put method. You should be able to see the values we send from the fetch request.

app.put("/quotes", (req, res) => {
  console.log(req.body);
});
Enter fullscreen mode Exit fullscreen mode

Put request body

The next step is to change the Yoda's first quote to this quote by Darth Vadar.

Changing Yoda's quote

MongoDB Collections come with a method called findOneAndUpdate. This method lets us find and change one item in the database. It has the following syntax:

quotesCollection
  .findOneAndUpdate(query, update, options)
  .then((result) => {
    /* ... */
  })
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

query lets us filter the collection with key-value pairs. If we want to filter quotes to those written by Yoda, we can set { name: 'Yoda' } as the query.

quotesCollection
  .findOneAndUpdate({ name: "Yoda" }, update, options)
  .then((result) => {
    /* ... */
  })
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

update, tells MongoDB what to change. It uses MongoDB's update operators like $set, $inc and $push.

We will use the $set operator since we're changing Yoda's quotes into Darth Vadar's quotes:

quotesCollection
  .findOneAndUpdate(
    { name: "Yoda" },
    {
      $set: {
        name: req.body.name,
        quote: req.body.quote,
      },
    },
    options
  )
  .then((result) => {
    /* ... */
  })
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

options tells MongoDB to define additional options for this update request.

In this case, it's possible that no Yoda quotes exist in the database. We can force MongoDB to create a new Darth Vadar quote if no Yoda quotes exist. We do this by setting upsert to true. upsert means: Insert a document if no documents can be updated.

quotesCollection
  .findOneAndUpdate(
    { name: "Yoda" },
    {
      $set: {
        name: req.body.name,
        quote: req.body.quote,
      },
    },
    {
      upsert: true,
    }
  )
  .then((result) => {
    /* ... */
  })
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

Finally, let's log the result into the command line.

app.put('/quotes', (req, res) => {
  quotesCollection.findOneAndUpdate(/* ... */)
    .then(result => {
      console.log(result)
     })
    .catch(error => console.error(error))
}
Enter fullscreen mode Exit fullscreen mode

Try clicking the "replace first Yoda quote" button in the browser. You should see this result in your command line. This says we changed one of Yoda's quote.

Put result.

If you refresh the browser, you should see Darth Vadar's quote as the first quote.

Checking the HTML after a PUT request.

:::note
Does the findOneAndUpdate look complicated to you? Well, It IS complicated. This is why I use Mongoose instead of MongoDB. You can find out more about Mongoose in this article.
:::

Finally, we need to respond to the JavaScript that sent the PUT request. In this case, we'll simply send the success message.

app.put('/quotes', (req, res) => {
  quotesCollection.findOneAndUpdate(/* ... */)
    .then(result => {
       res.json('Success')
     })
    .catch(error => console.error(error))
}
Enter fullscreen mode Exit fullscreen mode

Next, we can handle the response from the server via a then object. (We do this because fetch returns a promise). However, Fetch is slightly different from most promises. You need to use another then object to get the response from the server.

Here's what you should do:

fetch({
  /* request */
})
  .then((res) => {
    if (res.ok) return res.json();
  })
  .then((response) => {
    console.log(response);
  });
Enter fullscreen mode Exit fullscreen mode

You should be able to see a Success message from the server in the console.

Response from the server.

:::note
I wrote an article on the Fetch API if you're wondering why we need two then calls. Give it a read! It'll help cement your understanding.
:::

If you are working on a fancy webapp, you can use JavaScript to update the DOM, so users see the new changes immediately.

However, updating the DOM is out of scope of this article, so we're just going to refresh the browser to see the changes.

fetch({
  /* request */
})
  .then((res) => {
    if (res.ok) return res.json();
  })
  .then((response) => {
    window.location.reload(true);
  });
Enter fullscreen mode Exit fullscreen mode

:::note
If you want to learn to use JavaScript to update the DOM, I suggest going through my Learn JavaScript course. I even teach you how make your interface fast and snappy! (Check the Todolist Component).
:::

That's it for the UPDATE operation! Let's move on to delete.

CRUD - DELETE

The DELETE operation can be triggered through a DELETE request. It's similar to the UPDATE request so this should be simple if you understand what we've done above.

For this, let's delete the first quote by Darth Vadar.

First, we need to add a delete button to index.ejs.

<div>
  <h2>Remove Darth Vadar!</h2>
  <p>
    Delete one Darth Vadar's quote. Does nothing if there are no more Darth
    Vadar's quote
  </p>
  <button id="delete-button">Delete Darth Vadar's quote</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Adds delete button to the index.ejs file.

Then, we'll trigger a DELETE request through Fetch when a user clicks the delete button.

const deleteButton = document.querySelector("#delete-button");

deleteButton.addEventListener("click", (_) => {
  fetch("/quotes", {
    method: "delete",
  });
});
Enter fullscreen mode Exit fullscreen mode

Since we're deleting a quote by Darth Vadar, we only need to send Darth Vadar's name to the server.

deleteButton.addEventListener('click', _ => {
  fetch(/* ... */, {
    method: 'delete',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name: 'Darth Vadar'
    })
  })
    .then(res => {
      if (res.ok) return res.json()
    })
    .then(data => {
      window.location.reload()
    })
})
Enter fullscreen mode Exit fullscreen mode

We can then handle the event on our server side with the delete method:

app.delete("/quotes", (req, res) => {
  // Handle delete event here
});
Enter fullscreen mode Exit fullscreen mode

Deleting a document from MongoDB

MongoDB Collections has a method called deleteOne. It lets us remove a document from the database. It takes in two parameters: query and options.

quotesCollection
  .remove(query, options)
  .then((result) => {
    /* ... */
  })
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

query works like query in findOneAndUpdate. It lets us filter the collection to the entries we're searching for. In this case, we can set name to Darth Vadar.

quotesCollection
  .remove({ name: "Darth Vadar" }, options)
  .then((result) => {
    /* ... */
  })
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

However, since we already pass the name Darth Vadar from Fetch, we don't need to hardcode it in Express anymore. We can simply use req.body.name.

app.delete("/quotes", (req, res) => {
  quotesCollection.remove({ name: req.body.name }, options);
});
Enter fullscreen mode Exit fullscreen mode

In this case, we don't need to change any options, so we can omit options.

app.delete("/quotes", (req, res) => {
  quotesCollection.deleteOne({ name: req.body.name });
});
Enter fullscreen mode Exit fullscreen mode

Then, we can send a response back to the JavaScript in the then call.

app.delete("/quotes", (req, res) => {
  quotesCollection
    .deleteOne({ name: req.body.name })
    .then((result) => {
      res.json(`Deleted Darth Vadar's quote`);
    })
    .catch((error) => console.error(error));
});
Enter fullscreen mode Exit fullscreen mode

Now, when you click the delete button, the browser will sends DELETE request through Fetch to our Express server. Then, the server responds by sending either an error or a message back.

What if there are no more Darth Vadar quotes?

If there are no more Darth Vadar quotes, result.deletedCount will be 0. We can send a message that says tells the browser that there are no more Darth Vadar quotes to delete.

app.delete("/quotes", (req, res) => {
  quotesCollection
    .deleteOne(/* ... */)
    .then((result) => {
      if (result.deletedCount === 0) {
        return res.json("No quote to delete");
      }
      res.json(`Deleted Darth Vadar's quote`);
    })
    .catch((error) => console.error(error));
});
Enter fullscreen mode Exit fullscreen mode

If the JavaScript receives a No quote to delete response, we can tell the user there's no Darth Vadar quote to delete.

To do this, let's add an element where we can tell users about this message.

<div id="message"></div>
Enter fullscreen mode Exit fullscreen mode

If we receive No quote to delete, we can change the textContent of this .message div.

const messageDiv = document.querySelector("#message");

deleteButton.addEventListener("click", (_) => {
  fetch(/* ... */)
    .then(/* ... */)
    .then((response) => {
      if (response === "No quote to delete") {
        messageDiv.textContent = "No Darth Vadar quote to delete";
      } else {
        window.location.reload(true);
      }
    })
    .catch(/* ... */);
});
Enter fullscreen mode Exit fullscreen mode

Message if no quotes can be deleted.

That's it for the DELETE operation!

Make it look better...

The final step is to make the app look a little better by sprinkling some styles!

Add CSS to make the app look better.

Wrapping Up

We covered A LOT in this mega tutorial. Here's a list of things we've done together:

  1. Understood what Express, Node, and MongoDB are used for
  2. Understood CRUD
  3. Executed Create, Read, Update and Delete operations
  4. Created an Atlas account for MongoDB
  5. Save, read, update, and delete from MongoDB
  6. Display variable data with template engines

You have now learned all you need to know about creating simple applications with Node, Express, and MongoDB. Now, go forth and create more applications, young padawan. May the force be with you.

Grab the Source Code

You can grab the source code by leaving your name and email address in this form. I'll also send you this article in PDF so you can read it at your leisure.

Further reading

Here's some further readings if you're interested to continue with the Node, Express, MongoDB journey


Thanks for reading. This article was originally posted on my blog. Sign up for my newsletter if you want more articles to help you become a better frontend developer.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .