JavaScript is the top web development language today, running in millions of applications and websites. Modern JavaScript applications use REST APIs built with Node.js and a user interface built with a JavaScript framework such as React or Angular.
The most popular framework for building restful APIs is Express. It provides support for http requests out of the box and has an intuitive syntax for the standard http methods used in the REST principles.
For all your data storage concerns, you can easily integrate MongoDB with the native driver available to you. Using this stack, you can leverage MongoDB's document model with the REST API standard payload, which uses the JSON format.
This article will provide a step-by-step tutorial on how to use Express with MongoDB Atlas, our database-as-a-service platform, to expose restful API endpoints for our client-side application.
Table of contents
You can explore the complete project in the following:
main branch - finished project
stub branch - starting point for following this tutorial
Our application will follow the standard REST architectural style. You will have client and server components. The application will send client requests to our server. The server will then fetch the data from the MongoDB Atlas database and return it to the client.
Finally, our front-end application will be written in React to use the REST API endpoints hosted on the Express.js server. The application is a blogging platform that uses the sample_training database, containing a posts
collection.
Our back-end application will use the more modern Ecmascript module (ES Modules) syntax.
The application used for this tutorial will be a blogging platform.
Here are the main files in the project:
.
├── app
│ └── src
│ ├── App.tsx
│ ├── components
│ │ └── PostSummary.js
│ └── pages
│ ├── Archive.js
│ ├── Create.js
│ ├── Home.js
│ └── Post.js
└── server
├── .env
├── db
│ └── conn.mjs
├── index.mjs
├── loadEnvironment.mjs
└── routes
└── posts.mjs
The “server” directory hosts the Express.js server application and its dependencies. The main files here are:
.env
: Configuration file holding Atlas connection string details.
db/conn.mjs
: Exposes a global connection to the Atlas database.
index.mjs
: The main entry point for the Express server.
loadEnvironment.mjs
: Loads up the environment variables.
routes/posts.mjs
: Exposes the REST API endpoints and performs their business logic.
The “app” directory is where the front-end React application code resides. The main files here are:
App.tsx
: Front-end React code that manages the different routes of the front end.
/components
: A folder with the reusable components you will use.
/pages
: All application pages have their matching file in this folder.
First, you will need to deploy an Atlas cluster. You can follow the Getting Started with Atlas guide to learn how to create a free Atlas account, create your first cluster, and get your connection string to the database. Be sure to also load the sample dataset.
git clone -b stub
git@github.com:mongodb-developer/mongodb-express-rest-api-example.git
Let's go to the “server” directory of the project and install the needed packages:
cd mongodb-express-rest-api-example/server
npm install
You can start the server right away. It will automatically reload every time you save one of the files in the project.
npm run dev
Now, we are ready to connect our Express server to the MongoDB Atlas Cluster.
Edit the .env
file to assign the ATLAS_URI variable the value of your connection string. Replace the credentials with your database username and password.
ATLAS_URI=mongodb+srv://<username>:<password>@sandbox.jadwj.mongodb.net/myFirstDatabase?retryWrites=
PORT=5050
Our application uses the dotenv package to load this .env
file to add these values to the environment variables usable by the code.
You will need the following code in the loadEnvironment.mjs
file to load these environment variables.
import dotenv from "dotenv";
dotenv.config();
The entry point script can then import this file, which will be executed immediately. In index.mjs
, add the following line at the top.
// Load environment variables
import "./loadEnvironment.mjs";
The Atlas connection string will now be available to our other modules.
Next, open server/db/conn.mjs
and replace the content with the following code. This code will create a global database object that the other server components can reuse.
import { MongoClient } from "mongodb";
const connectionString = process.env.ATLAS_URI || "";
const client = new MongoClient(connectionString);
let conn;
try {
conn = await client.connect();
} catch(e) {
console.error(e);
}
let db = conn.db("sample_training");
export default db;
This code uses the connection string provided in the .env
file and creates a new client. Once the client is defined, it tries to create a new connection to the database. The sample_training
database is then exported if the connection is successful. This gives us a uniform interface that can be reused in all modules.
The core of this tutorial is to expose REST API routes to perform Create, Read, Update, and Delete (CRUD) operations for our restful web service.
In this sample application, all the routes for our server are located in the file server/routes/posts.mjs
. We then tell our server to use this module for all the incoming requests to routes that start with /posts
. This is done in the index.mjs
file in the following line.
// Load the /posts routes
app.use("/posts", posts);
The code is already there, so you don't need to add that. We will now focus on the logic for all of the API requests.
There will be six routes, each performing one of the CRUD operations.
The Read route will be returning 50 of the articles when there is a get request on the /posts
route. In server/routes/posts.mjs
, find the GET route and replace the code with the following.
// Get a list of 50 posts
router.get("/", async (req, res) => {
let collection = await db.collection("posts");
let results = await collection.find({})
.limit(50)
.toArray();
res.send(results).status(200);
});
Here, we start by defining our collection object, then do a .find
operation. We limit the results and convert the response data to an array. We can then send data to the client using res.send
. We can also specify the status code with the status
method. Code 200 stands for "OK, " meaning that the operation was successful.
You can also create a more complex route using aggregation pipelines to return a result. For example, we can add a route that will return the three most recent articles in the collection.
// Fetches the latest posts
router.get("/latest", async (req, res) => {
let collection = await db.collection("posts");
let results = await collection.aggregate([
{"$project": {"author": 1, "title": 1, "tags": 1, "date": 1}},
{"$sort": {"date": -1}},
{"$limit": 3}
]).toArray();
res.send(results).status(200);
});
As you can see here, the code is similar. This endpoint will catch all the get requests to /post/latest
. We then use an aggregation pipeline to sort the collection in descending order of date and limit the results to three. This leverages the database to do the heavy lifting with our data.
You can also use parametrized routes to return filtered results or, in this case, a single object. Notice in the following code snippet that the route takes an :id
parameter. You can then access the value of that parameter with req.params.id
.
// Get a single post
router.get("/:id", async (req, res) => {
let collection = await db.collection("posts");
let query = {_id: ObjectId(req.params.id)};
let result = await collection.findOne(query);
if (!result) res.send("Not found").status(404);
else res.send(result).status(200);
});
In this get request, the id is converted into an ObjectId
using the method provided by the MongoDB native driver. It is a query filter to retrieve a single document with this unique id.
If no results are found, we can send a different response — a 404 error message, in this case.
The Create route will add a new post to our collection. To catch a post request, you will use the router.post
method from Express to define this route. Based on the REST conventions, adding new items should be done with a POST method.
// Add a new document to the collection
router.post("/", async (req, res) => {
let collection = await db.collection("posts");
let newDocument = req.body;
newDocument.date = new Date();
let result = await collection.insertOne(newDocument);
res.send(result).status(204);
});
In this sample, we take the request body, append the current date and time, and save it in the collection direction with the collection.insertOne() method.
You can also use InsertMany to insert multiple documents at once.
The Update route adds a new comment to our blog post. Best practices in REST API design state that we should use a PATCH request for updates. Sometimes, a PUT request might also be used.
You might wonder why this is considered an update and not a create. This is because we are leveraging the MongoDB document model. Since we are not restricted to a flat structure like a relational database, we store all the comments as an array inside the post object.
To add a comment, we will update this array with a push operator with the request body as the new comment.
// Update the post with a new comment
router.patch("/comment/:id", async (req, res) => {
const query = { _id: ObjectId(req.params.id) };
const updates = {
$push: { comments: req.body }
};
let collection = await db.collection("posts");
let result = await collection.updateOne(query, updates);
res.send(result).status(200);
});
The route uses the collection.updateOne() method with the unique id specified as an ObjectId, and the operator as the second argument.
And last but not least, we need to handle the delete request. The users will have the ability to delete an article from our blog.
// Delete an entry
router.delete("/:id", async (req, res) => {
const query = { _id: ObjectId(req.params.id) };
const collection = db.collection("posts");
let result = await collection.deleteOne(query);
res.send(result).status(200);
});
Here, we are using a parametrized route to get the id of the object to delete. The post is then deleted with the collection.deleteOne() method.
Your server is now ready to be used. You can already test it with tools such as Postman or curl. For example, you could run the following command to get the latest articles.
curl localhost:5050/posts/latest
Now, any third-party applications can connect to this server, but let's see how we can get our React application to connect.
Plenty of tutorials provide a lot more detail on how to query a public API and manage responses from a client request. Understanding the basics of React applications is out of the scope of this article, but you can see examples of how to access the API you just created in the /app/src
folder.
In the /app/src/pages/Home.js
file, we retrieve the latest three entries from the blog. This is done through a request to the /posts/latest
route of the server. In this application, we are using the browser's native fetch
object.
const loadPosts = async () => {
let results = await fetch(`${baseUrl}/posts/latest`).then(resp => resp.json());
setPosts(results);
}
This loadPosts
function uses fetch
and passes the URL to the API. We could use the response in several data formats, but we are explicitly converting it to JSON, as it's the easiest way to manipulate data in JavaScript. The setPosts
function updates the application's state and forces the user interface to re-render.
All requests to read from the API follow the same pattern. We use the following to fetch all the articles in the app/src/pages/Archive.js
page.
let results = await fetch(`${baseUrl}/posts/`).then(resp => resp.json());
The only difference here is the route. The same goes for fetching a single document in app/src/pages/Post.js
.
let results = await fetch(`${baseUrl}/posts/${params.id}`).then(resp => resp.json());
We still use the fetch
object to create a new entry, but you will need to specify the HTTP method, an additional http header, and the request body. The code to create, update, or delete entries can be found in /app/src/pages/Create.js
.
await fetch(`${baseUrl}/posts`, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
author, title, tags: tags.split(","), body
})
}).then(resp => resp.json());
Doing an update follows the same patterns but with a different method.
await fetch(`${baseUrl}/posts/comment/${params.id}`, {
method: "PATCH",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
author, body
})
});
And the same is true for a delete request.
await fetch(`${baseUrl}/posts/${params.id}`, {
method: "DELETE"
});
If you want to test the application, open a new terminal and run the following.
cd ../app
npm install
npm start
Once all components are up and running, we can open the http://localhost:3000
URL, and you should see the sample blog. From there, you can create new blog posts, read an existing entry, add a comment, or delete that entry.
You now have a fully functional application that uses the REST principles. It has a back end that answers the necessary requests, and the client can request data from those endpoints.
Atlas App Services, MongoDB's development cloud services, offer a robust and scalable replacement to the self-hosted Express Server.
Using Custom HTTPS Endpoints, you can convert this application into a serverless RESTful API. Using serverless technology will make your application much more scalable.
Using Express as a back-end framework is a popular MongoDB stack design. Express is lightweight and approachable for JSON and REST API operations. MongoDB Atlas is a scalable and flexible document database as a service and makes a perfect companion to Express in many stacks like MERN, MEAN, and MEVN.
Atlas App Services and custom HTTPS endpoints are a robust replacement for the Express tier, removing the need to manage an Express server and its dependencies on-prem.
The MongoDB Driver is the native driver for Node JS applications. Therefore, the Express JS server can use it to access MongoDB.
Visit the following links: