REST API with Node.js
REST is a powerful and ubiquitous paradigm for web application development. It offers huge benefits that can help any service be more efficient, more extensible, and more scalable.
Node.js is a popular solution for building fast, efficient, and scalable APIs. It is highly extensible, offering an ecosystem of packages that deliver fast and unified development.
Combining REST with Node.js can result in a powerful product that is stable while being efficient, scalable, and extensible. What is very important, however, is making a truly RESTful output – adopting REST is only the first step in becoming RESTful, and there are as many roads to RESTful design as there are pitfalls on the way there. Today, we’re going to look at what REST is, what makes something RESTful, and how you can get started in RESTful development with Node.js.
What is REST?
REST, or Representational State Transfer, is an approach to developing systems. In essence, it allows systems to communicate with one another through a stateless modality wherein the representation of the resource is shared rather than the resource itself. Every interaction in a RESTful implementation expresses the state representation alone, which allows for efficient and seamless interactions across a wide variety of implementations.
Basics of REST
REST, or Representational State Transfer, was introduced in the famous seminal REST dissertation by Roy Fielding. It outlined a new approach to networked communication centered around a state transfer paradigm. Although REST can encompass various attributes, its essence can be summarized in key principles:
-
REST utilizes a ’’Client-Server relationship’’. In this model, the client and server operate in separate domains, each managing its own state. Interaction occurs through a simple exchange: the client requests, and the server responds. This singular mode of communication underpins the REST architecture.
-
REST is ’’’stateless’’’. Here, “state” refers to the current conditions or the data of a resource. In a stateless framework, the client handles state management. Thus, each server request must carry all necessary information to be processed and responded to independently, ensuring that requests are self-sufficient.
-
REST has a ‘’’uniform interface’’’. Consistency is key in RESTful interfaces - all components adhere to a standard interface, ensuring uniformity in interactions regardless of the specific service or resource. This includes adhering to a single URI for resources and, where applicable, incorporating additional references or data. The addition of references or data in context is a concept known as HATEOAS (Hypermedia as the Engine of Application State), and is considered a requirement of RESTful design.
-
REST must be ‘’’cacheable’’’. Effective cache management is crucial in REST. Servers designate data as cacheable or non-cacheable, significantly impacting performance and user experience.
-
REST is layered. RESTful components enable distributed architecture, allowing for a setup where clients may not directly interact with the end server, but instead interact through various intermediary services and microservices.
What Makes a RESTful API RESTful?
What makes something truly RESTful? One good approach is to use the Richardson Maturity Model. This model from Leonard Richardson allows us to use a standard rubric for considering the stage of development for a project, bucketing services within a handful of categories:
Level 0: The Swamp of POX
This is the lowest rung of the model, and represents an API with a URI which accepts all inputs. It is considered an API in name only without anything that sets it aside as RESTful. For this reason, ‘’’Level 0 is Non-RESTful’’’.
Level 1: Resources
At this level, resources are defined, and users can make requests of the resource URI. Instead of asking a remote resource or function to do something, you ask the method of the resource to do it. ‘’’Level 1 is Approaching REST’’’.
Level 2: HTTP verbs
Level 2 sees the introduction of HTTP verbs, the underlying verbiage for the web and RESTful APIs. In Level 2, HTTP verbs have a specific meaning, form, and function, whereas in lower levels, these verbs are often used for a variety of functions. At level 2, however, GET means GET, POST means POST, etc., and they are not conflated. Since REST requires caching, and HTTP only does caching with proper verbiage use ‘’’Level 2 is Approaching REST with Caching’’’.
Level 3: Hypermedia controls
At level 3, we see the addition of HATEOAS, or Hypertext as the Engine of Application State. HATEOAS allows for relational links to additional context or resources, and is a requirement of REST. ‘’’Level 3 is Likely RESTful’’’.
What is “Likely RESTful”?
Why is level 3 only “likely RESTful”? There are some requirements in the original dissertation that are a bit more variable in application. For this reason, something can be in level 3 but still be missing some very specific functions. In order to be truly RESTful, you need to meet all the requirements of the original dissertation – accordingly, being RESTful should include reaching Level 3 of the Richardson Maturity Model while also properly implementing the other characteristics of the original dissertation!
What is Node.js?
Node.js is an open-source and cross-platform JavaScript implementation specifically designed for server-side scripting, allowing code to execute outside of a browser. It supports a wide variety of platforms, and is designed with the concept of “JavaScript Everywhere”, unlocking a synchronicity between client and server-side development through the adoption of a universal language for web applications and those clients who consume those applications.
Notably, Node.js is event-driven, and supports both real-time and synchronous communication as well as asynchronous communication. This allows Node.js to support a huge range of potential development options, and presents a unified approach for blended environments utilizing both synchronous and asynchronous modalities.
Node.js has some key features that make it a strong choice:
- Node.js uses non-blocking event-driven architectural design, but has libraries that enable blocking functions. In essence, this provides a unified option for both synchronous and asynchronous development utilizing JavaScript.
- Because the underlying technology is event-driven and non-blocking, Node.js can handle a lot of connections concurrently, providing for extreme efficiency in network and application communication.
- Since it’s built on Chrome’s V8 JavaScript Engine, JavaScript can be directly compiled to machine code, unlocking quite high performance, pretty incredible speed, and high efficiency.
- Node.js comes with npm (Node Package Manager), a large ecosystem of open-source libraries and extensions. This manager also manages the various dependencies, installation processes, updates, etc. that come with utilizing this ecosystem, simplifying the development lifecycle.
- Full-lifecycle integration is achieved readily. Because Node.js unifies the process of developing client and server-side code with JavaScript, development and maintenance can be unified into the same lifecycle process.
These benefits have made Node.js a popular choice for fast, scalable, and efficient network applications with a simplified lifecycle.
How to Create a RESTful API with Node.js
To get started, we’re going to utilize the Express framework. Express is a Node.js framework that is minimalist, flexible, and efficient. By combining Node.js and Express together, you can unlock a lot of additional design modalities and functions with very little overhead or impact on speed.
Setting Up Your Development Environment
This guide assumes that you have already installed Node.js. If you have not, navigate to the Node.js website and ensure that you have installed it. From here, you’ll want to install Express.
Installing Express
To install express, you’ll need to create a director to hold your Node.js application. Use the mkrdir to do so as follows:
mkdir restnode
Next, use the cd command to change the working directory to this new location:
cd restnode
From here, you’re going to use npm, the built-in package manager for Node.js, to create the pack.json file for your application.
npm init
While the default settings can be used for this process, Express notes in its documentation that you need to set the entry point variable to change the main file name. We’re going to use the standard default – this will generate index.js, which is where we’re going to build our service. If you wanted to change this, you would edit the following code:
entry point: (yourname.js)
Finally, we install express:
npm install express
Create the API Skeleton
To get started, we must first set up the basic skeleton of our API. To do this, we must edit our javascript as follows:
const express = require(‘express’);
const app = express ();
This will create the base of our service, pointing towards Express as our framework. Next, we need to tell the application that we are using a few critical pieces:
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use("/api/users", require("./routes/api/users"));
This code does a few things, but the most important thing it does is enable POST and PUT requests by giving a method for data storage. “app.use(express.json());” allows our service to process JSON objects, and “app.use(express.urlencoded({ extended: false }));” allows us to process data as strings or arrays.
Now that we have created the skeleton, we need to actually set a listening port. To do this, use this code:
app.listen(3000, () => console.log('Ready'));
This code instructs our service to listen to port 3000, and to log to the console a “Ready” state, which lets us know it is ready to process requests.
Create the User Model
While we could connect to a database (for more information on this, check the Express documentation, we’re going to keep this simple by using a local file for storing user data. To do this, we must first create the Users.js file.
We can do this again by using the mkdir command. Use the following code to create this file:
touch Users.js
This will create the file for us to edit. Now that we have the file, we need to create the actual data structure which can be used by other systems. To do this, set the data structure using the following code:
const users = [
{
id: 1,
name: "ExampleUser",
email: "exampleuser@website.com"
}
];
With this structure in place, we can add a module export at the end to allow other project files to use this structure. To do this, append the following code to users.js:
module.exports = users;
Create the Route Structure
Next, we need to create our routes and endpoints. To do this, we’re going to create a new directory folder and call it “routes”, and then a subfolder called “api”. This will allow us to store any and all routing data in a separate directory, cleaning up our structure a bit and clarifying exactly where points of failure may be occurring in the future during troubleshooting. You can use the “mkdir” operation here to do so.
In this folder, we will create a new .js file called users.js. Note that we have used a lowercase “u” here – it’s important to remember nomenclature and standards, and as we already used the “Users” namesake for the storage solution, we’re using “users” to define this a subordinate file that contains our routes.
Create the Endpoints
In this file, we need to create some logical routes. To start, again state that we require Express:
const express = require(‘express’)
Next, we need to define the router by using this code:
const router = express.Router();
In order to make sure our data routing is clear and pulls in data in a structured way, we’re going to need a way to generate unique IDs for each user entity. To do this, first use npm to install the “uuid” package in our core directory:
npm install uuid
Back in our Users.js file, we can call this “uuid package” using the following:
const uuid = require("uuid");
Next, we need to set the path for the API so that our user system can handle this data correctly. We can use the following code to do so:
let users = require("../../Users");
Now we can finally create our first endpoint. We’ll create a GET function to retrieve all user data. We can do this using this code:
router.get("/", (req, res) => {
res.json(users);
});
This code uses route.get to handle the request, and passes all of the current user data stored in the system. To get a specific ID, we need to provide a way for the client to pass a user ID and check against the internal data store. We can use the following code to do so:
router.get("/:id", (req, res) => {
const found = users.some(user => user.id === parseInt(req.params.id));
if (found) {
res.json(users.filter(user => user.id === parseInt(req.params.id)));
} else {
res.sendStatus(400);
}
});
Note that there is an “if” function here which provides a generic error code when the request is malformed or is referencing non-existent data.
With this route created, we can now export this for use by the API using the following code:
module.exports = router;
Now we have a functional API which will present user data on request. Neat!
Implement CRUD Functions
From here, we can expand these endpoints to provide a variety of new functions. CRUD, or Create, Read, Update, Delete, is a critical concept for RESTful APIs, and should be the core functions in your application. We can enable each using the following code.
For creating user data, we can use POST:
router.post("/", (req, res) => {
const newUser = {
id: uuid.v4(),
name: req.body.name,
email: req.body.email
};
if (!newUser.name || !newUser.email) {
return res.sendStatus(400);
}
users.push(newUser);
res.json(users);
});
For updating, we can use PUT:
router.put("/:id", (req, res) => {
const found = users.some(user => user.id === parseInt(req.params.id));
if (found) {
const updateUser = req.body;
users.forEach(user => {
if (user.id === parseInt(req.params.id)) {
user.name = updateUser.name ? updateUser.name : user.name;
user.email = updateUser.email ? updateUser.email : user.email;
res.json({ msg: "User updated", user });
}
});
} else {
res.sendStatus(400);
}
});
For deleting, we can use DELETE:
router.delete("/:id", (req, res) => {
const found = users.some(user => user.id === parseInt(req.params.id))
if (found) {
users = users.filter(user => user.id !== parseInt(req.params.id))
res.json({
msg: "User deleted",
users
});
} else {
res.sendStatus(400);
}
});
Make it RESTful
One of the big things that is lacking here is HATEOAS. HATEOAS, or Hypermedia at the Engine of Application State, provides relational links within our user data and is a sub-constraint of RESTful design. To accomplish this, we can use a wide variety of options – for our case, we’re going to use a very simple Express extension called “express-hateoas-links”.
To get started, first install express-hateoas-links:
npm install express-hateoas-links
With this installed, we need to start adding in additional context to our “routes” logic. As an example, for our GET logic, we would edit the following:
router.get("/:id", (req, res) => {
const found = users.some(user => user.id === parseInt(req.params.id));
if (found) {
res.json(users.filter(user => user.id === parseInt(req.params.id)));
} else {
res.sendStatus(400);
}
});
The edit would look like this:
router.get("/:id", (req, res) => {
const foundUser = users.find(user => user.id === parseInt(req.params.id));
if (foundUser) {
const userWithLinks = {
...foundUser,
links: [
{ rel: "self", method: "GET", href: `/users/${foundUser.id}` },
{ rel: "all-users", method: "GET", href: "/users" },
{ rel: "update-user", method: "PUT", href: `/users/${foundUser.id}` },
{ rel: "delete-user", method: "DELETE", href: `/users/${foundUser.id}` }
]
};
res.json(userWithLinks);
} else {
res.sendStatus(400);
}
});
This update would add some relational links. “rel” would establish the relations of each contextual link, and then would provide additional contextual links based upon the particular method used to interact with the original node. While these relational links are simply used for function extension, additional relational links – such as context about the particular person, links to their organizations, etc. – could also be provided through this method.
Best Practices
Authentication and Authorization
It’s crucial to implement authentication and authorization measures in your service based on the least privilege principle. This approach involves restricting access rights for users to the bare minimum necessary to perform their tasks. It’s essential to carefully manage access to your resources, ensuring they are as secure as possible.
Be judicious in your use of hypermedia; while it can be beneficial, indiscriminately exposing all resources isn’t advisable. Strive to share data that is both useful and secure, minimizing the risk of misuse or compromise.
API Structure
When designing RESTful APIs, it’s important to be resource-oriented. Each resource should be associated with a specific HTTP verb that performs a distinct function, and these interactions should be idempotent (and safe, although that’s a much broader topic than the scope of this piece). Idempotency means that each request should consistently produce the same type of response, even though the actual content may vary.
Offering caching capabilities to clients is a key consideration, but make sure your cache is not caching information that may be protected or utilized for attacks, such as API Keys, user data, login methods, etc.
Additional Reading
Reference documentation as much as you can – this has been done before, and well, so learn from others! We have used the following pieces throughout this article, and we recommend the following as excellent follow-up documentation and content:
- Roy Fielding’s original REST dissertation
- Express’ documentation
- Database integration options
- Ravikiran’s wonderful Simplilearn walkthrough
- Toptal’s secure development guide by Marcos Henrique da Sila
Conclusion
It’s easy to make something REST, but to make it truly RESTful can be a bit more complicated. With some planning and an approach based in context and relational linking, it can be incredibly easy to get started making a Node.js application that is powerful, extensible, and stable.
Let us know what you think about this piece by leaving a comment in the section below. Thank you for reading!