How to Use Express to Build a REST API
Did you know that in 2019, Express ranked number one in awareness, interest, and satisfaction, according to theĀ State of JS, a yearly survey that had more than 20,000 respondents? It is mostly used to create robust APIs for the web, quickly and easily through its flexible API.
APIs are common means of communication between different software components. They provide a simple way to exchange data between two applications. In our case, this will be between the browser and a database. In this tutorial, weāre going to build a scalable REST API in Node using Express.
To keep things simple, we will go with the classical todo example. We will build an API to store, retrieve, modify, and delete todo items. Each operation will be handled by a different HTTP request method. Our very first job will be to set up Express.

Setting Up Express
To make this tutorial concise and digestible, I will replace the database functionality withĀ LocalStorage
. Of course, we donāt have this in node so we will have to polyfill it. This means we will have two dependencies:Ā express
Ā andĀ node-localstorage
.Ā npm init -y
Ā your project and add these to your dependencies.
{
"name": "express-api",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "4.17.1",
"node-localstorage": "2.1.5"
}
}
I also replaced the default script withnode server.js
; this is the file where we will set up the Express server. Create theĀ server.js
Ā file in your root directory and add the following lines to it:
const express = require('express'),
app = express(),
port = process.env.PORT || 8080;
app.listen(port);
console.log(`API server is listening on port:${port}`);
We can start the webserver withĀ app.listen
Ā passing in the port; either from the command line or defaulting to 8080. Not much is happening right now. If you openĀ localhost:8080
, youāll see the server doesnāt return anything. So letās change that and add some routes!

Creating Routes
For the routes, Iāve created a separate directory calledĀ routes
Ā and added anĀ index.js
. Weāre going to have four different endpoints:
GET
Ā for getting all or a single todo itemPOST
Ā for creating a new todo itemPUT
Ā for updating an existing todo itemDELETE
Ā for removing a specific todo item
This is how ourĀ routes/index.js
Ā will look like:
'use strict';
const routes = (app) => {
const todo = require('../controllers/Todo');
// Todo Route
app.route('/todo/:id?/')
.get(todo.get)
.post(todo.create)
.put(todo.update)
.delete(todo.delete);
};
module.exports = routes;
routes
Ā will be a function that gets the express app as a parameter. TheĀ app
Ā variable exposes aĀ route
Ā method which takes in an endpoint as a parameter. We can specify route params by using colons. By also adding a question mark at the end, we can tell express that this is only an optional param.
OnĀ route
, we can chain different HTTP request methods. For every method, we will execute a different function. The methods are coming from an object defined in the controllerās folder underĀ Todo.js
, so that will be our next step.
But first, to actually tell Express to use these routes, go back to yourĀ server.js
Ā file and extend it with the following:
const express = require('express'),
routes = require('./routes/index'),
app = express(),
port = process.env.PORT || 8080;
routes(app);
app.listen(port);
console.log(`API server is listening on port:${port}`);
Iāve importedĀ routes
Ā and passed the ExpressĀ app
Ā to it. Now if you navigate toĀ localhost:8080/todo
Ā it will call theĀ todo.get
Ā method which we havenāt specified yet, so letās do that right now.

Requests and Responses
If you havenāt already, create aĀ controllers
Ā folder and add aĀ Todo.js
Ā file. Weāre going to export an object containing four methods for the four requests:
const LocalStorage = require('node-localstorage').LocalStorage;
const localStorage = new LocalStorage('./db');
module.exports = {
get(request, response) {
},
create(request, response) {
},
update(request, response) {
},
delete(request, response) {
}
};
Each method will get access to aĀ request
Ā andĀ response
Ā object. We also need to import theĀ LocalStorage
Ā package since weāre going to use that in place of a real database. It will automatically create aĀ db
Ā folder for you in the root directory.
Letās go in order and see how we can get back todos using theĀ get
Ā method.
Get route
We want to either get all or a specific todo, based on whether the id has been provided in the URL or not. We also want to check whether we have aĀ localStorage
Ā item set, so we donāt end up with an error. This leaves us with the following checks:
get(request, response) {
if (localStorage.getItem('todos')) {
if (!request.params.id) {
// Return all todos
} else {
// Return single todo
}
} else {
// No todos set on localStorage, fall back to empty response
}
}
To get URL parameters, we simply need to access theĀ request.params
Ā object. The name of the property will be the one specified inĀ app.route
.Ā (:id
)Ā To return a JSON response, we can callĀ response.json
Ā with an object we want to return as a response:
get(request, response) {
if (localStorage.getItem('todos')) {
if (!request.params.id) {
response.json({
todos: JSON.parse(localStorage.getItem('todos'))
});
} else {
const todo = JSON.parse(localStorage.getItem('todos')).filter(todo => todo.id === parseInt(request.params.id, 10));
response.json({
todo
});
}
} else {
response.json({
todos: []
});
}
}
If we donāt even haveĀ todos
Ā inĀ localStorage
, we can return an empty array. Otherwise, we can return the items stored in localStorage. Since we can only store strings, we need to callĀ JSON.parse
Ā on the object. The same applies when we want to access a single todo. But this time, we also want to filter for a single item.
If you refresh the page, youāll get back an emptyĀ todo
Ā list.

Post route
Letās populate the array with some items. This time, we want to send the request data using aĀ x-www-form-urlencoded
Ā content type. Since we canāt send aĀ POST
Ā request right inside the browser without any frontend, we need to find another way. For this task, Iām using the popularĀ PostmanĀ app. You can download and install it for free.
Open the app and create a new request. Set the method type toĀ POST
Ā and the body toĀ x-www-form-urlencoded
. We only want to add a new todo if aĀ name
Ā and aĀ completed
Ā flag have been provided.

To get the values from the request inside Express, we can accessĀ request.body
. If you, however, send a post request and try to log outĀ request.body
, youāll notice that it isĀ undefined
. This is because express by default canāt handle URL encoded values. To make them accessible through JavaScript, we have to use a middleware. Add the following line to yourĀ server.js
Ā file, before you define the routes:
app.use(express.urlencoded({ extended: true }));
Now if you send theĀ POST
Ā request and you try to log outĀ request.body
Ā again, youāll get the values logged out to your console.

So we can start by checking whether we have the two values in the request and if not, we can send an error specifying the problem:
create(request, response) {
if (request.body.name && request.body.completed) {
// Add new todo
} else {
response.json({
error: 'ā ļø You must provide a name and a completed state.'
});
}
}
The way we want to add a new item is we simply want to get theĀ todos
Ā from localStorage if thereās any, parse the JSON and push a new object to the array. Then convert it back to JSON, and of course, send a response to let us know if we were successful.
if (request.body.name && request.body.completed) {
const todos = JSON.parse(localStorage.getItem('todos')) || [];
todos.push({
id: todos.length,
name: request.body.name,
completed: request.body.completed === 'true'
});
localStorage.setItem('todos', JSON.stringify(todos));
response.json({
message: 'Todo has been successfully created. š'
});
}
Note that since we might not haveĀ todos
Ā present in the localStorage, we need to fall back to an empty array. Also note that since weāre getting the requests as strings, we need to cast theĀ completed
Ā flag to a boolean.

Put route
Once we have enough items on our todo list, we can try to update them. Again, we need to check for the presence of an id and either aĀ name
Ā or aĀ completed
Ā flag.
update(request, response) {
if (request.params.id && (request.body.name || request.body.completed)) {
// Update todo
} else {
response.json({
error: 'ā ļø You must provide an id and a property to update.'
});
}
}
We want to follow a similar logic we did for theĀ create
Ā method: Parse the localStorage data, update the item in the array where the id matches the one passed as a request param, convert the data back to JSON and send a success response:
if (request.params.id && (request.body.name || request.body.completed)) {
const todos = JSON.parse(localStorage.getItem('todos'));
todos.forEach(todo => {
if (parseInt(request.params.id, 10) === todo.id) {
todo.name = request.body.name || todo.name;
if (request.body.completed) {
todo.completed = request.body.completed === 'true';
}
}
});
localStorage.setItem('todos', JSON.stringify(todos));
response.json({
message: 'Todo has been successfully updated. š'
});
}
Remember that we want to cast theĀ completed
Ā flag into a boolean. And the reason why we canāt do logical OR just like we did forĀ todo.name
Ā is because in case we wantĀ completed
Ā to be set to false, it would always fall back to the defaultĀ todo.completed
Ā value.

Delete route
Probably the shortest and simplest method of all will be theĀ delete
. All we have to do is filter out the item where the id matches the one passed into the endpoint:
delete(request, response) {
if (request.params.id) {
const todos = JSON.parse(localStorage.getItem('todos')).filter(todo => todo.id !== parseInt(request.params.id, 10));
localStorage.setItem('todos', JSON.stringify(todos));
response.json({
message: 'Todo has been successfully removed. šļø'
});
} else {
response.json({
error: 'ā ļø You must provide an id.'
});
}
}
And reassign its stringified version back toĀ todos
Ā inside localStorage.

Conclusion
Now you have a working API in place to handle todo items. Iāll leave the UI part up for you. The great way about this approach is that every operation is separated into a different method. This way your API is more easily scalable. It also helps reducing time looking for bugs. If you are experiencing a problem with one of the requests, you can quickly pinpoint where and what went wrong. Youāll know that the problem lies in one single function.
If you were wondering about the look and feel of the JSON response I was getting throughout the tutorial, Iām using theĀ JSON ViewerĀ Chrome extension, which you can get at the provided link. If you would like to mess around with the final project, you can reach it at theĀ express-apiĀ Github repo.
Thank you for reading through. Whether if you have any experience building APIs and working with Express or not, share your thoughts in the comments below and let us know what is your approach.
ā Get yourself an Expresso Sticker ā

Continue the tutorial, by learning how to also secure your freshly created API with JSON Web Tokens:

Unlimited access to hundred of tutorials
Access to exclusive interactive lessons
Remove ads to learn without distractions