In the last article, we covered about bootstrapping an Express.js project. The main focus of that tutorial was mainly on getting started with setting the project up and getting to know the workflow. In this article, we get our hands dirty and start coding some stuff. The main focus is on building rest APIs with Express.js.
An imaginary use case
It is always better to have a very simple scenario in mind and start implementing the code. Here, we follow the same trend. For that, we prepared a very rudimentary use case to get started with.
Let say we want to create an app that has some rest APIs for User related operation. This means that via the APIs we should be able to do simple CRUD operations. The list is as follows:
- Get all users
GET /api/v1/users
- Create a user
POST /api/v1/users/
- Get a user by id
GET /api/v1/users/{id}
- Remove a user by id
DELETE /api/v1/users/{id}
- Update a user by id
PUT /api/v1/users/{id}
For better understanding let’s have a look at the below diagram,
Implementing APIs with Express.js
So now that we’re clear on what the requirement is, it’s time to get our hands dirty and start coding. We can use the previous project template and start the coding right away.
The first thing we have to do is to create an HTTP server. For that, we can bootstrap the server as follows,
import express from "express";
import { restart } from "nodemon";
const PORT = 8090;
const app = express();
app.listen(PORT, () => {
console.log("Server has started");
});
In the first line, we import the express
and nodemon
libraries. The former is for creating the HTTP server and the later is for updating the app without restarting the server. Then we create an instance of it and assign it to the app
variable. And finally, instruct the app to listen to the port 8090
.
Now if you open your browser and type http://localhost:8090
, you should see Cannot GET /
message on the screen.
That means we can start implementing the endpoints.
Implementing GET /api/v1/users endpoint
The first and the simplest endpoint is /api/v1/users
which retrieves all the users. Since we don’t have a database connection, yet, for now, we return dummy/mock results.
The endpoint accepts a GET
request and returns a collection of Users
,
app.get("/api/v1/users", (req, res, next) => {
res.status(200).send({
users: [
{ userId: 1, firstName: "John", lastName: "Wick", age: 45 },
{ userId: 2, firstName: "Kasra", lastName: "Mp", age: 30 }
]
});
});
Now add the above code and save the changes. Then if you hit http://localhost:8090/api/v1/users
again, you should see some mock results.
Implementing POST /api/v1/users endpoint
The next step is to create an API to create a user. Again, since there’s no database connection, we can’t save the result anywhere. We just return the result on whether the user is successfully created or not.
Since the user information is passed in the request body, we need to be able to parse it. By default, Express.js doesn’t have such a feature. For that, we need to rely on body-parser
as I’ve stated in the previous article.
So let’s import body-parser
and configure it as follow,
import bodyParser from "body-parser";
const app = express();
app.use(bodyParser.urlencoded());
app.use(bodyParser.json());
app.use(
bodyParser.urlencoded({
extended: true
})
);
The above instructs the express
app to use body-parser URL encoder
and JSON parser
. This way we can parse the request body easily.
Now we can code the endpoint as follow,
app.post(
"/api/v1/users",
(req, res, next) => {
const { firstName, lastName, age } = req.body;
let ageInt = parseInt(age);
if (firstName && lastName && ageInt) {
next();
} else {
res.status(400).send({
error: "The payload is wrong!"
});
}
},
(req, res) => {
const randomUserId = Math.floor(Math.random() * 65536);
res.status(201).send({
userId: randomUserId,
firstName: req.body.firstName,
lastName: req.body.lastName,
age: parseInt(req.body.age)
});
}
);
The above code consists of two sections. The first part is validation and the second part is supposed to be saving the user to DB and return the result. For that part, so far we just return the result and skip storing the result to DB.
In regards to the validation first, we destruct the request body and extract user’s details. Then we validate the required details. If they are available, using next()
we instruct the code to execute the second part. Otherwise, we reject the request by returning 400
and a description of what’s wrong.
In the second part, we just return the same request body with the randomly generated id associated with that user. Ideally, the id should come from the database.
Implementing GET /api/v1/users/{id} endpoint
The implementation of this endpoint is a little bit tricky. Because before processing it we need to validate the id
is an integer and then react accordingly. To implement that we have two options:
- validation middleware before handling the request
- validation in a
route
method
Note: middleware in Express.js is similar to Spring or Tomcat filter which executes before the routing happens. It is a good place to implement some validation and avoid polluting the controller.
If the same validation should be done on multiple endpoints, better to do it in middleware. Or even if the validation is lengthy. Otherwise, the business logic and validation part will be mixed up.
In our case, we use middleware for the validation. For that, let’s create a middleware that validates all the requests that come to /api/v1/users/{id}
regardless of the request method.
app.use("/api/v1/users/:id", function(req, res, next) {
if (req.params.hasOwnProperty("id") && !isNaN(parseInt(req.params.id))) {
next();
} else {
console.log("Request is invalid!");
res.status(400).send({ error: "User id should be a number" });
}
});
In the above code first, we check whether id
is provided and if yes, whether it’s number or not. If yes, we proceed further which means executing the controller. Otherwise, it returns bad request 400
with a reason.
We can implement the endpoint as follows,
app
.route("/api/v1/users/:id")
.get(function(req, res) {
res.status(200).send({
userId: parseInt(req.params.id),
firstName: "John",
lastName: "Wick",
age: 45
});
})
The endpoint is very similar to the previous one. The only difference is here, we directly parse the request param to int since there’s validation before. Again here we return the mock result.
Implementing DELETE /api/v1/users/{id} endpoint
From now onwards we can extend the same route /api/v1/users/:id
and just keep adding new HTTP method. For DELETE
it would be like this,
app
.route("/api/v1/users/:id")
.get(function(req, res) {
res.status(200).send({
userId: parseInt(req.params.id),
firstName: "John",
lastName: "Wick",
age: 45
});
})
.delete(function(req, res) {
let userId = parseInt(req.params.id);
res.status(204).send();
})
As you can see, we just extended the same route and just add a new method which starting on line 11
.
Implementing PUT /api/v1/users/{id} endpoint
Last but not least is the update API. This endpoint is tricker than the other two because it has some custom validation,
.put(function(req, res) {
let userId = parseInt(req.params.id);
const { firstName, lastName, age } = req.body;
const ageInt = parseInt(age);
if (firstName && lastName && ageInt) {
res.status(200).send({
userId: userId,
firstName: firstName,
lastName: lastName,
age: ageInt
});
} else {
res.status(400).send({
error: "The payload is wrong!"
});
}
});
So when a PUT
request hits /api/v1/users/{id}
endpoint, first the middleware validation is executed and if everything is alright, the above code executes which also has some validations. It checks for availability of firstName
, lastName
and if everything is alright, returns a mock result. Otherwise, responses with 400
bad request.
Error handling for unexpected errors
We are done with the APIs but it is always good to have a middleware to handle uncaught exceptions as follows,
app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send("Internal server error");
});
So if the app has some unexpected behavior, this method executes and returns internal server error instead of the stack trace.
Conclusion
That’s all for this article. In the next article, we go through a database connection. So that we can return the real results instead of mock ones. As always, the fully functional demo is available on GitHub.
Inline/featured images credits
- Computer desk by Oskar Yildiz on Unsplash