As a developer, you'll definitely have to consume an API or even build one at some point in your work life. What I intend to do with this post is to show how to build a simple REST API in which we can save user data(names and emails) to a local MongoDB database, update data, delete data and view data, so essentially we are going to implement CRUD operations.
Requirements
We are going to need the following tools and technologies for this project;
- MongoDB (check out my post on how to install mongoDB)
- You should know how to use mongoDB to create and carry out other operations on a database.
- Node and npm (you can download it here)
- VS Code. (Download it here).
- REST Client - a VS code extension which we are going to use to test our API we could as well use Postman(a platform for API development) but as a way to keep everything in VS code, we will use REST Client(you can download it here).
With that out of the way let's begin. Start by creating a new directory for our project. I named mine node-api
.cd
into the directory and run the following commands;
npm init -y
this commands creates apackage.json
file for our project.npm i express mongoose
it installs Express and Mongoose .npm i --save-dev dotenv nodemon
installs two development-only dependencies.
After having installed all the project dependencies above, we can start creating files and writing our API's code in them. The first file we are going to create is a .env
. So go ahead and create it inside the root directory of our project. We are going to place environment variables such as the Database URL,
port and other important stuff we do not want to include in our code directly for security reasons in the .env
file. The dotenv dependency we installed earlier will make it possible for us to pull in environment variables from this .env
file. The next file we have to create is the index.js
file which is kind of like our main file.After creating the index file, replace the script section of our package.json
file with the code below.
"scripts": {
"devStart": "nodemon index.js"
}
Setting Up Our Server
Add the code below to your .env
file.
PORT = 8000
Add the following code to index.js
.
const express = require("express");
const app = express();
const mongoose = require("mongoose");
require("dotenv").config();
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server is up and running on ${PORT}`));
What the code above does is it imports the dependencies we install earlier on with npm and starts our server on the specified port.
Connecting to Our MongoDB Database
The next thing we have to do in our index file is to create a connection to our database so add the code below to the file.
mongoose.connect(process.env.DATABASE_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on("error", (error) => console.error(error));
db.once("open", () => console.log("Connected to Database"));
So the code we just wrote initiates a connection to our database and listens for whether there was an error or the connection was a success. To make sure that everything functions as required, add the your DATABASE_URL variable to the .env
file. I created a mongoDB database called users
so my .env
file looks like this.
DATABASE_URL = "mongodb://localhost/users"
PORT = 8000
Now run npm run devStart
to test our database connection. If our terminal output is similar to that in the picture below then everything is working as expected.
Now let's make it possible for our server to accept JSON data. Add this code to our index file just before the app.listen()
line.
app.use(express.json())
Theuse
method in the code above is a middleware that allows us to run code when the server gets a request but just before it gets passed to our routes. So Express will accept data from the database in a JSON format.
Creating and Setting up Our Routes
We are going to create a folder for our routes could routes
in the root directory and inside this routes
folder, we will create a users.js
file. Let's tell our server that we now have a file for our routes by requiring the file we just created in our index.js like this.
const usersRouter = require("./routes/users");
At this point our index file should look like this.
What we are going to do inside the routes users.js
file is to define how the server handles data when it receives an HTTP POST, GET, PATCH or DELETE request. Let's add some code to this file.
const express = require('express')
const router = express.Router()
// Get all users
router.get('/', (req, res) => {
})
// Create one user
router.post('/', (req, res) => {
})
// Get one user
router.get('/:id', (req, res) => {
})
// Delete one user
router.delete('/:id', (req, res) => {
})
// Update one user
router.patch('/:id', (req, res) => {
})
module.exports = router;
So what the code above does is it imports express, creates a Router instance and defines all the routes that are useful to our project. The routes functions we have created don't do much now. We will get back to them soon.
Making the Model
It is ideal that we define our model in a folder of its own, with that in mind let's create a Models
directory for model files and in it let's create a user.js
file. The reason for this naming convention is that user.js
file defines how a single user's data should look like as opposed to the users.js
file in the routes directory that can be used to carry out operations like a GET request on multiple users. Now let’s go ahead and setup our model and its schema. A schema is how our API defines what the data looks like. Add the code below to user.js
.
const mongoose = require('mongoose')
const userSchema = new mongoose.Schema({});
module.exports = mongoose.model("User", userSchema);
So the code requires mongoose, defines a schema and export it which allow us to use and interact with our database using the schema. Mongoose has a special way of exporting models using mongoose.model() that takes two arguments as shown in the code above. Inside the empty object that is passed as an argument to the schema instance we made above, update the schema so it now looks like this.
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
dateAdded: {
type: Date,
required: true,
default: Date.now,
},
});
The type
and required
properties are pretty self explanatory. They are defining the expected schema type (a String and Date in our case) as well if that key is required upon receiving information for a new user.
One thing to note about dateAdded
property is that we set the type to Date
instead of String
since we will be expecting a date from the user. If no date is provided then we default it to the current date by using Date.now
. The finished schema should look like this.
Now that we have written our model's code and exported it, let's require it in our users.js
file in the routes directory. Add this code to the file after the first two lines of code.
const User = require("../models/user");
Now we can continue from where we ended with our routes and we shall tackle them one after the other starting with the route to Get all users. Update the get all users route to look like this.
// Get All Users
router.get('/', async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch(err) {
res.status(500).json({ message: err.message });
}
})
The code we have written above sends an HTTP GET request whose callback function is wrapped as a promise with a try/catch statement to retrieve all user data from our database and converts the data to JSON if the request was successful or catch an error if there was one and set the response status to 500 which means an internal server error occurred.
Now that we have our route to get all the users in our database, we need to write code that will enable us to actually add a user into our database. So, lets move onto our Create one user route so we can create and store user data.
router.post("/", async (req, res) => {
const user = new User({
name: req.body.name,
email: req.body.email
});
try {
const newUser = await user.save();
res.status(201).json(newUser);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
You can see its somewhat similar to our Get All Users route except for few important differences. First off, we’re no longer sending a GET request to our database but a POST request which will allow us to send data to our database. We are creating a variable user
that will be assigned to a new User from the model we created earlier. If you recall, we require a name, email and dateAdded properties for a new user though dateAdded defaults to the current time if one isn't supplied by the user. We used the save() Mongoose method instead of find() because this is how we will tell the database that we want it to store the information a user passes to us through this router function. The last parts of the code sends the user a response with a success status of 201 chained with the just submitted user data in a JSON format. The catch is similar to that of the Get All Users route except for the fact that we pass a 400 error since this would be a user error for passing us malicious data.
Testing Our Get All Users and Post Routes
Now the time has come for us to test the routes we have just implemented to see that they are working as they should. Like I said earlier we are going to user the REST Client VS code extension for this. You could as well use Postman. So create a routes.rest
file in the root directory of our project. Copy the following code into the routes.rest
file.
GET http://localhost:3000/users
###
POST http://localhost:3000/users
Content-Type: application/json
{
"name": "John Doe",
"email": "johndo@gmail.com"
}
If you click on the Send Request link just before POST localhost:3000/users, it saves the name John Doe
and email johndoe@yahoo.com
to the database. If the POST request was successful you should see a response tab like the one in the image below.
To test our Get All Users route click on the Send Request link just above GET localhost:3000/users . You would see a response tab like the one in the image below if the GET request was successful.
We’re now in the final lap of this RESTful API race! The last things we have to do is complete our Delete A User, Update A User and Get A User routes and our API will be ready. The Delete, Update and Get A User routes all have one thing in common which is getting the ID of a specific user and using that ID to perform an operation. So instead of writing that part of the repeating that piece of code three times, we can just put it in its own function and pass it as a middleware in the remaining routes we have to write code for. Let's put this middleware function named getUser right before the line where we export our routes file.
async function getUser(req, res, next) {
try {
user = await User.findById(req.params.id);
if (user == null) {
return res.status(404).json({ message: "Cant find user" });
}
} catch (err) {
return res.status(500).json({ message: err.message });
}
res.user = user;
next();
}
There is quite a lot going on in that middleware function so let's break that down. From the top the function kind of looks familiar except for a new parameter next
that has been passed to it. Basically, what next
does when it is called is to tell the function execution to move onto the next section of our code, which is the route function the getUser function has been added to. Then we have a try/catch statement where we try to find a user by their ID or catch an error if there was something wrong with the request. Now let's look at the last two lines in there.
res.user = user
and next()
.
The res.user
line is setting a variable on the response object which is equal to our user object. This is useful so we don’t have to write that same line of code again, we can just reference res.user
from this function. Lastly, we use the next()
function after everything else has finished executing to tell the getUser function to move onto the actual request that was sent.
Now that we have created our middleware function, let's implement the remaining routes starting with Get A User route. Update the code for that route to this.
// Get A user
router.get('/:id', getUser, (req, res) => {
res.json(res.user);
})
See what our middleware did for us there? It enables us to write as minimal code as possible since searching for a user by their specific ID has been abstracted to the middleware. Let's test this route real quick to make sure our getUser function and the new route we just created actually work as they should. So we are going to send another POST request so create a new user.
So we created a new user called Jamie Lanister
and we can see he has a long ID associated with his object right above his name in the response tab. I will copy that ID so when we write our new GET route I can call Jamie by his unique ID. We can put this below our Get All Users request so our routes.rest
file now looks like this.
GET http://localhost:8000/users
###
GET http://localhost:8000/users/6073c2ae2072c0830c73daf6
###
POST http://localhost:8000/users
Content-Type: application/json
{
"name": "Jamie Lanister",
"email": "jamie@hotmail.com"
}
Note: Your user's ID will obviously be different from mind so be sure to copy your own user's ID.
So if everything went well with our Get A User request we should get only a single object from our database which is Jamie's.
Delete A User
Now it's time for us to write the code for this route so without further ado let's get to that.
// Delete A user
router.delete('/:id', getUser, async (req, res) => {
try {
await res.user.remove();
res.json({ message: "User Deleted" });
} catch (err) {
res.status(500).json({ message: err.message });
}
})
I assume what is happening is not unfamiliar to you. We have our old friend the try/catch statement in which we try to delete a specific user and if that operation was successful we get a "User Deleted" message or catch the error that occurred.
Update A User
The last route we have to implement is the update route. We want it to be in a way that a user can update just the name or email and both the name and the email. So we essentially have to check and see if any changes were made and if changes were made, update them appropriately. Now onto the code:
// Update A User
router.patch("/:id", getUser, async (req, res) => {
if (req.body.name != null) {
res.user.name = req.body.name;
}
if (req.body.email != null) {
res.user.email = req.body.email;
}
try {
const updatedUser = await res.user.save();
res.json(updatedUser);
} catch {
res.status(400).json({ message: err.message });
}
});
Our Update route starts off with a PATCH method. Now you can see we’ve added two if statements to our function. The first if statement is checking to see if the name coming from the body of the user’s request is not null. This is a crucial check because if it is null it means the user did not pass any name through our route function. If they did pass a name we move onto this line:
res.user.name = req.body.name
Where we’re setting our user's name from res.user
and setting the name now equal to the new name that the user passed in from their PATCH request.
The same logic is used in the code below:
res.user.email = req.body.email
Where we’re checking to see if the user updated their email and if they did, we then perform the same operation of changing the current email to the new one from the user’s request.
After we’ve done these if statement checks we then want to tell the function to save these new changes to our database. This is easily done within our try statement where we take the res.user
object with our new name and/or email and then add the save() method onto it within a new variable called updatedUser. We then want to pass this new updatedUser object to our user in a JSON format.
So that is that about our routes file, we have fully implemented all our CRUD operation but before we move on to do our final test, I will humbly implore you to check that we are on the same page with our code bases. So go to this GitHub Repo and compare codes to make sure that you haven't made a mistake up to this point.
Final Tests
After haven implemented all our routes, the moment of truth has come - time to make sure that all the routes are working as they should but since we have tested most of the routes except our Delete and Update routes, let's test them real quick starting with the Delete Route. So add the code below to you routes.rest
file after our POST request.
####
DELETE http://localhost:8000/users/<a-user's-id>
Remember to change a <a-user's-id>
to an actual ID in your database. Now click Send Request
to see if our user is successfully deleted.
Voila, the user whose ID is passed as a parameter to the DELETE request has been deleted as you can see in the image above. Now if you take that same ID that you just deleted and try to make a Get A User request with it, it should tell us that it cannot find that user since the user no longer exist in our database. Let's try that.
Now let's test the Update route which is our last route. I just created a new user with the name Tyrion Lanister
and we are going to use this user to test our Update A User route.
So now I am going to send a PATCH request to update the name Tyrion Lanister
to Jon Snow
. I'm putting my PATCH request right after the POST request in my routes.rest
file.
If you look at the response tab you'd see that the name was update successfully. So all routes are working as expected. Yeyyyy!!!
Conclusion
Woww that was quite long! But you still made to the end 🎉👏🏽. This is the longest article I have ever written and I know it is worth the time I spent on it because I enjoyed writing it and I hope it has taught you something. We have covered quite a lot in this post and it is easy to get overwhelmed. What I have to say is that it is okay to feel frustrated or overwhelmed sometimes but never stop being curious and wanting to learn more. Please do not hesitate to leave a comment down below in the comment section if you got stuck or found something in the code that can be made better. Connect with me on twitter @flaacko_flaacko and LinkedIn at Brandon Bawe. Till my next post, Happy Hacking.