Introduction
REST APIs are an industry-standard way for web services to send and receive data. They use HTTP request methods to facilitate the request-response cycle and typically transfer data using JSON, and more rarely - HTML, XML and other formats.
In this guide, we are going to build a REST API to manage books with Node.js and Express.
For the sake of simplicity, we won't be using a database, so you don't need experience using one. We will use a simple JavaScript array to store our data instead.
What is a REST API?
REST (Representational State Transfer) is a standard architecture for building and communicating with web services. It typically mandates resources on the web are represented in a text format (like JSON, HTML, or XML) and can be accessed or modified by a predetermined set of operations. Given that we typically build REST APIs to leverage HTTP instead of other protocols, these operations correspond to HTTP methods like GET, POST, or PUT.
On a collection of data, like books for example, there are a few actions we'll need to perform frequently, which boil down to - Create, Read, Update and Delete (also known as CRUD Functionality).
An API (Application Programming Interface), as the name suggests, is an interface that defines the interaction between different software components. Web APIs define what requests can be made to a component (for example, an endpoint to get a list of books), how to make them (for example, a GET request), and their expected responses.
What is Express?
ExpressJS is one of the most popular HTTP server libraries for Node.js, which by default isn't as friendly for API development. Using Express, we simplify API development by abstracting away the boilerplate needed to set up a server, which makes development faster, more readable and simpler. You can spin up a prototype API in seconds and a couple of lines of code.
Although its primary use was to simplify things with sensible defaults, it's highly customizable using functions called "middleware".
Note: Express is very lightweight and is built on top of middleware. Using middleware, you can expand and extend its functionality beyond the functions already present by default.
Even though we are only going to build a REST API in this guide, the ExpressJS framework is not limited to just that - hosting static files, performing server-side rendering, or even using it as a proxy server isn't uncommon and the sky's the limit with additional middleware.
HTTP Request Types
There are a few types of HTTP methods that we need to grasp before building a REST API. These are the methods that correspond to the CRUD tasks:
POST
: Used to submit data, typically used to create new entities or edit already existing entities.GET
: Used to request data from the server, typically used to read data.PUT
: Used to completely replace the resource with the submitted resource, typically used to update data.DELETE
: Used to delete an entity from the server.
Note: Notice that you can use either POST
or PUT
to edit stored data. You're free to choose whether you even want to use PUT
since it can be omitted fully. Though, stay consistent with the HTTP verbs you use. If you're using POST
to both create and update, then don't use the PUT
method at all.
What We are Going to Build
Let's create a simple app to store information about books. In this app, we will store information about the ISBN of the book, title, author, published date, publisher and number of pages.
Naturally, the basic functionality of the API will be CRUD functionality. We'll want to be able to send requests to it to create, read, update and delete Book
entities. Of course, an API may do much more than this - provide users with an endpoint to get statistical data, summaries, call other APIs, etc.
Non-CRUD functionalities are application-dependent, and based on your project's nature, you'll probably have other endpoints. However, practically no project can go without CRUD.
To avoid making up book data - let's use a dataset from GitHub to get some sample details about books.
Setting Up the Project
First, let's initialize a new Node.js project:
$ npm init
Fill the requested information to your requirements - you don't have to fill all of the fields, but they're an easy way to set up identifiable data for a project. Fields like the name
are much more relevant for publishing applications to the Node Package Manager, amongst other fields.
Alternatively, you can use the default settings by adding the -y
flag to the call:
$ npm init -y
Either way - you'll end up with a project with a package.json
file. This is a json
file that contains all the relevant metadata on your project and will look something along these lines by default:
{
"name": "app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"keywords": [],
"description": ""
}
The "entry"/"main" of your application is the file to run to start the project correctly - usually your main script, and index.js
by default.
Additionally, the version of your application and "scripts" are here! You can supply any number of custom commands in the scripts
section, with a command associated with an alias. Here, the test
alias is a wrapper for an echo
statement.
You'd run the test of the application via:
$ npm test
> [email protected] test /Users/david/Desktop/app
> echo "Error: no test specified" && exit 1
Error: no test specified
Oftentimes, there's a start
alias that masks one or more processes that should be run when we want to start an application. In the basic form - we just run the index page with node:
{
"name": "app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
},
"author": "",
"license": "ISC",
"keywords": [],
"description": ""
}
You can put any number of commands besides node index.js
as the start
script and when you run npm start
- they'll all run:
$ test npm start
> [email protected] start /Users/david/Desktop/app
> node index.js
Note: Since we only have one command in the starting script, it's functionally equivalent to just calling $ node index.js
in the command line to start the application.
Now that you're familiar with the project, let's install Express!
$ npm install --save express
A new file is created in the directory, alongside a node_modules
directory. The package-lock.json
file keeps track of your dependencies and contains their versions and names:
{
"name": "app",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
...
The node_modules
directory actually hosts the code of the dependencies, and can get quite large very quickly. Just by installing Express, we've already got a hefty number of modules installed and tracked in the package-lock.json
file.
These modules are, factually, small so it's not an issue by any means. By using the package-lock.json
file, another client would know which dependencies to download and which versions to use to correctly be able to start up your application.
Note: When doing version control with tools like Git - it's considered a good practice not to version the source code of the modules you use in the application. In practical terms - don't keep track or push node_modules
to a repository. Others can download the dependencies based on the crucial package-lock.json
which happens automatically when they run the application with npm
.
Creating a Simple Endpoint
Now, let's start building a simple "Hello World" app. It'll have a single simple endpoint that just returns a message as a response to our request to get the home page.
First, let's create a file called hello-world.js
:
$ nano hello-world.js
Then, let's import the Express framework within it:
const express = require('express');
Next, we'll want to instantiate the Express app:
const app = express();
And set our port:
const port = 3000;
The port will be used a bit later, when we tell the app to listen to requests. These three lines are boilerplate - but the great thing is, that's all the boilerplate there is!
Now, we can create a simple GET
endpoint right beneath the boilerplate. When a user hits the endpoint with a GET request, the message "Hello World, from express" will be returned (and rendered in the browser or displayed on the console).
We'd like to set it to be on the home page, so the URL for the endpoint is /
:
app.get('/', (req, res) => {
res.send('Hello World, from express');
});
At this point, let's start our clients:
app.listen(port, () => console.log(`Hello world app listening on port ${port}!`))
Let's run the application and visit the only endpoint we have via our browser:
$ node hello-world.js
Hello world app listening on port 3000!
This is technically a working API! Though, this endpoint doesn't really do much. Let's take a look at some common middleware that'll be useful for further work and create some more useful endpoints.
Express Middleware
As mentioned above - ExpressJS is a simple HTTP server and it does not come with a lot of features out of the box. Middleware acts almost like extensions for the Express server and provides additional functionalities in the "middle" of a request. Many third-party extensions like morgan for logging, multer for handling file uploads, are used routinely.
For now, to get started, we need to install a middleware called body-parser, which helps us decode the body from an HTTP request:
$ npm install --save body-parser
It parses the body of the request and lets us react to it accordingly.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Since we are calling the API from different locations by hitting endpoints in the browser. We also have to install the CORS middleware.
If you're not yet familiar with cross-origin resource sharing, it is okay for now. Let's just install the middleware and configure it:
$ npm install --save cors
Building a REST API with Node and Express
Adding Books
Now we can start building our app. Create a new file called book-api.js
:
const express = require('express')
const bodyParser = require('body-parser');
const cors = require('cors');
const app = express();
const port = 3000;
// Where we will keep books
let books = [];
app.use(cors());
// Configuring body parser middleware
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.post('/book', (req, res) => {
// We will be coding here
});
app.listen(port, () => console.log(`Hello world app listening on port ${port}!`));
As you can see, we can configure body-parser
by importing it and passing it to the app.use
method, which enables it as middleware to the Express app
instance.
We will be using the books
array to store our collection of books, simulating a database.
There are a few types of HTTP request body types. For example, application/x-www-form-urlencoded
is the default body type for forms, whereas application/json
is something we'd use when requesting a resource using jQuery or a backend REST client.
What the body-parser
middleware will be doing is grabbing the HTTP body, decoding the information, and appending it to the req.body
. From there, we can easily retrieve the information from the form - in our case, a book's information.
Inside the app.post
method let's add the book to the book array:
app.post('/book', (req, res) => {
const book = req.body;
// Output the book to the console for debugging
console.log(book);
books.push(book);
res.send('Book is added to the database');
});
Now, let's create a simple HTML form with the fields: ISBN, title, author, published date, publisher, and number of pages in a new file, say new-book.html
.
We'll be sending the data to the API using this HTML form's action
attribute:
<div class="container">
<hr>
<h1>Create New Book</h1>
<hr>
<form action="http://localhost:3000/book" method="POST">
<div class="form-group">
<label for="ISBN">ISBN</label>
<input class="form-control" name="isbn">
</div>
<div class="form-group">
<label for="Title">Title</label>
<input class="form-control" name="title">
</div>
<!--Other fields-->
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
Here, our <form>
tag's attribute corresponds to our endpoint and the information we send with the submit
button is the information our method parses and adds to the array. Note that the method
parameter is POST
, just like in our API.
You should see something like that when you open the page:
Clicking "Submit", we're greeted with the our applications console.log(book)
statement:
{ isbn: '9781593275846',
title: 'Eloquent JavaScript, Second Edition',
author: 'Marijn Haverbeke',
publish_date: '2014-12-14',
publisher: 'No Starch Press',
numOfPages: '472' }
Note: Please note that since we are using an array to store data we will lose them in our next app restart.
Getting All Books
Now let's create an endpoint to get all the books from the API:
app.get('/books', (req, res) => {
res.json(books);
});
Restart the server. If the server is already running press Ctrl + C
to stop it first. Add some books and open http://localhost:3000/books
in your browser. You should see a JSON response with all the books that you've added.
Now let's create an HTML page to display these books in a user-friendly way.
This time around, we'll create two files - book-list.html
which we'll use as a template and a book-list.js
file which will hold the logic to updating/deleting books and displaying them on the page:
Let's start off with the template:
<div class="container">
<hr>
<h1>List of books</h1>
<hr>
<div>
<div class="row" id="books">
</div>
</div>
</div>
<div id="editBookModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Book</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form id="editForm" method="POST">
<div class="form-group">
<label for="ISBN">ISBN</label>
<input class="form-control" name="isbn" id="isbn">
</div>
<div class="form-group">
<label for="Title">Title</label>
<input class="form-control" name="title" id="title">
</div>
<!--Other fields-->
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
</div>
<!--Our JS file-->
<script src="book-list.js"></script>
With the template done, we can implement the actual logic to retrieve all books using browser-side JavaScript and our REST API:
const setEditModal = (isbn) => {
// We will implement this later
}
const deleteBook = (isbn) => {
// We will implement this later
}
const loadBooks = () => {
const xhttp = new XMLHttpRequest();
xhttp.open("GET", "http://localhost:3000/books", false);
xhttp.send();
const books = JSON.parse(xhttp.responseText);
for (let book of books) {
const x = `
<div class="col-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">${book.title}</h5>
<h6 class="card-subtitle mb-2 text-muted">${book.isbn}</h6>
<div>Author: ${book.author}</div>
<div>Publisher: ${book.publisher}</div>
<div>Number Of Pages: ${book.numOfPages}</div>
<hr>
<button type="button" class="btn btn-danger">Delete</button>
<button types="button" class="btn btn-primary" data-toggle="modal"
data-target="#editBookModal" onClick="setEditModal(${book.isbn})">
Edit
</button>
</div>
</div>
</div>
`
document.getElementById('books').innerHTML = document.getElementById('books').innerHTML + x;
}
}
loadBooks();
In the above script, we are sending a GET
request to the endpoint http://localhost:3000/books
to retrieve the books and then creating a Bootstrap card for every book to display it. If everything is working correctly you should see something like this on your page:
You probably noticed the Edit and Create buttons and their respective methods. For now, let's leave them empty and implement them as we go.
Retrieving a Book by ISBN
If we'd like to display a specific book to the user, we'll need a way to retrieve it from the database (or the array, in our case). This is always done by a key specific to that entity. In most cases, each entity has a unique id
that helps us identify them.
In our case, each book has an ISBN which is unique by nature, so there's no need for another id
value.
This is typically done by parsing the URL parameter for an id
and searching for the book with the corresponding id
.
For an example, if the ISBN is 9781593275846
the URL would look like, http://localhost:3000/book/9781593275846
:
app.get('/book/:isbn', (req, res) => {
// Reading isbn from the URL
const isbn = req.params.isbn;
});
Here, we're introduced to parameterized URLs. Since the ISBN depends on the book, there's potentially an infinite number of endpoints here. By adding a colon (:
) to the path, we can define a variable, mapped to the variable isbn
. So, if a user visits localhost:3000/book/5
the isbn
parameter will be 5
.
You can accept more than one parameter in your URL if it makes sense in your scenario. For example /image/:width/:height
, and then you can get those parameters using req.params.width
and req.params.height
.
Now, using our endpoint, we can retrieve a single book:
app.get('/book/:isbn', (req, res) => {
// Reading isbn from the URL
const isbn = req.params.isbn;
// Searching books for the isbn
for (let book of books) {
if (book.isbn === isbn) {
res.json(book);
return;
}
}
// Sending 404 when not found something is a good practice
res.status(404).send('Book not found');
});
Again restart the server, add a new book, and open localhost/3000/{your_isbn}
and the application will return the book's information.
Deleting Books
When deleting entities, we typically delete them one by one to avoid big accidental data loss. To delete items, we use the HTTP DELETE
method and specify a book using its ISBN number, just like how we retrieved it:
app.delete('/book/:isbn', (req, res) => {
// Reading isbn from the URL
const isbn = req.params.isbn;
// Remove item from the books array
books = books.filter(i => {
if (i.isbn !== isbn) {
return true;
}
return false;
});
res.send('Book is deleted');
});
We are using the app.delete
method to accept DELETE
requests. We have also used the array filter
method to filter out the book with the relevant ISBN to remove it from the array.
Now let's implement the deleteBook
method in the book-list.js
file:
const deleteBook = (isbn) => {
const xhttp = new XMLHttpRequest();
xhttp.open("DELETE", `http://localhost:3000/book/${isbn}`, false);
xhttp.send();
// Reloading the page
location.reload();
}
In this method, we are sending the delete request when the button is pressed and reloading the page to display the changes.
Editing Books
Very similar to deleting entities, updating them requires us to snatch a specific one, based on the ISBN and then send either a POST
or PUT
HTTP call with the new information.
Let's go back to our book-api.js
file:
app.post('/book/:isbn', (req, res) => {
// Reading isbn from the URL
const isbn = req.params.isbn;
const newBook = req.body;
// Remove item from the books array
for (let i = 0; i < books.length; i++) {
let book = books[i]
if (book.isbn === isbn) {
books[i] = newBook;
}
}
res.send('Book is edited');
});
Upon sending a POST
request, aimed at a specific ISBN, the adequate book is updated with new information.
Since we have already created the edit modal, we can use the setEditModal
method to gather information about the book when the "Edit" button is clicked.
We will also set the form's action
parameter with the clicked book's URL to send the request:
const setEditModal = (isbn) => {
// Get information about the book using isbn
const xhttp = new XMLHttpRequest();
xhttp.open("GET", `http://localhost:3000/book/${isbn}`, false);
xhttp.send();
const book = JSON.parse(xhttp.responseText);
const {
title,
author,
publisher,
publish_date,
numOfPages
} = book;
// Filling information about the book in the form inside the modal
document.getElementById('isbn').value = isbn;
document.getElementById('title').value = title;
document.getElementById('author').value = author;
document.getElementById('publisher').value = publisher;
document.getElementById('publish_date').value = publish_date;
document.getElementById('numOfPages').value = numOfPages;
// Setting up the action url for the book
document.getElementById('editForm').action = `http://localhost:3000/book/${isbn}`;
}
To verify if the update function works, edit a book. The form should be filled with the existing information about the book. Change something and click "Submit" after which you should see a "Book is edited" message.
Conclusion
That's how easy it is to build a REST API using Node.js and Express. You can visit the official Express documentation to learn more about the framework if you are interested.
Also, the code that I have provided is just for the sake of the tutorial, you should never use it in a production environment. Make sure you validate data and follow best practices when you write code for production.
As usual, the source code of this project can be found on GitHub.