RESTful API Design - The basics
Older Article
This article was published 8 years ago. Some information may be outdated or no longer applicable.
I recently delivered a training course on building RESTful APIs with Node.js. During that class, loads of questions came up around API design. Let’s look at some of the basics, in the context of Node.js and Express.
Most of the concepts here apply to any framework, inside or outside the Node.js ecosystem.
This article covers the basics only. Entire books exist on RESTful API design. A quick search will turn them up if you want to go deeper.
Introduction to REST
REST stands for REpresentational State Transfer. It’s an architectural approach to designing web services. At its most basic level, it uses hypermedia. Most people don’t realise that REST isn’t necessarily tied to HTTP, though most implementations do use HTTP because its communication protocol fits well.
A RESTful API doesn’t have to run over HTTP. It can use other transfer protocols like SNMP, SMTP, and so on.
HTTP is a communication protocol. REST is an architectural design. They’re separate things.
Basic design principles
Six basic design principles shape REST APIs. These are the foundation.
Resources
An API typically exists to solve a business problem. The word “business” is key. Before anything else, decide which entities (resources) you’ll expose. Think this through carefully at the start because it shouldn’t change later.
What counts as a business entity? Orders, products, customers, shipments, departments, employees. Essentially, we’re talking about nouns in plural form.
These entities don’t have to map one-to-one with database tables. A customer entity might pull data from multiple sources. What matters is that the API consumer gets all the customer data in a single, clean representation. If customer information lives in two databases across three tables, you can still assemble it into one entity.
Resource Identifiers
Every resource needs an identifier. The industry calls this a URI (Uniform Resource Identifier). The critical rule: they must be unique. Each one should identify exactly one entity.
Resource identifiers for individual entities must be truly unique. Use UUIDs or (in MongoDB’s case) the ObjectId. Avoid sequential UUIDs in distributed systems; they’re a performance killer.
Access resources through their URIs. They typically look like this:
http://domain.com/departments
Firing an HTTP GET against that URI retrieves all departments. Think of it as a collection: the departments collection holds all the individual department entities.
Resource hierarchy
Once the entities and URIs are sorted, think about hierarchy. How do relationships between entities get represented?
Say we’re building an API for an HR system. Entities might include: Departments, Employees, and Salaries.
Now consider the hierarchy. Employees belong to Departments. Employees have Salaries. That gives us three collections (three URIs) for listing all Departments, all Salaries, and all Employees.
To fetch a single department, pass in its ID:
http://domain.com/departments/12
To list all employees in that department:
http://domain.com/departments/12/employees
To get details about a specific employee at a specific department:
http://domain.com/departments/12/employees/1568
To find that employee’s salary:
http://domain.com/departments/12/employees/1568/salaries
That’s stretching things. Implementing deeply nested routing like this is hard to build and cumbersome to use.
A better approach: use HATEOAS for navigation between related resources. We’ll cover that later.
Resource representation
Next, define the data structure for responses. What data comes back, and in what format, when someone requests information about a given employee at a particular department?
These days, the standard format for RESTful APIs is JSON (though XML works too).
The JSON structure your API returns should be decoupled from the underlying database. If you’re using a relational database with schemas, a schema change shouldn’t break the API’s entity representation.
How to achieve that decoupling is a separate topic, but the principle holds regardless of whether you’re running a relational or NoSQL database.
There are many approaches to data modelling. In the Node.js world, most developers use NoSQL databases like MongoDB and often return data straight from the database. That can work since MongoDB stores a JSON-like structure (called BSON).
NoSQL and relational databases don’t compete with each other. Each has valid use cases with different strengths and weaknesses. A well-designed system can use both. The API can gather data from multiple databases and present a unified entity to the consumer.
HTTP methods for requests
Next up: the common HTTP methods.
GET- returns a resource. Either a full collection or an individual resource, based on the URI (http://domain.com/departmentsorhttp://domain.com/departments/12).POST- creates a new resource at the specified URI. Requires a payload containing the resource details.PUT- creates or replaces a resource at the specified URI. The payload contains the resource details.PATCH- applies a partial update to a resource at the specified URI. The payload contains the changes.DELETE- removes a resource at the specified URI.
There are subtle but important differences between
PUTandPATCH. Read this article to learn more.
Related course
HTTP methods and Express
All these HTTP methods are available through Express in Node.js. Here’s a conceptual implementation:
const express = require('express');
const app = express();
const port = 3000;
const router = express.Router();
const bodyParser = require('body-parser');
const cors = require('cors');
app.use(bodyParser.json());
// return all departments
router.get('/departments', handlerFn1);
// return a specific department
router.get('/departments/:id', handlerFn2);
// return all employees from a specific department
router.get('/departments/:id/employees', handlerFn3);
// create a new department
router.post('/departments', handlerFn4);
// create a new employee at a specific department
router.post('/departments/:id/employees', handlerFn5);
// bulk update departments
router.put('/departments', handlerFn6);
// update specific department if exists
router.put('/departments/:id', handlerFn7);
// bulk update employees at a department
router.put('/departments/:id/employees', handlerFn8);
// partially update a specific department
router.patch('/departments/:id', handlerFn9);
// partially update a specific employee at a specific department
router.patch('/departments/:id/employees/:id', handlerFn10);
// remove all departments
router.delete('/departments', handlerFn11);
// remote a specific department
router.delete('/departments/:id', handlerFn12);
// remove all employees at a specific department
router.delete('/departments/:id/employees', handlerFn13);
app.use('/api', router);
app.listen(port, () => console.info(`Server is listening on port ${port}`));
The code above is missing a few cases (like listing all employees), but it gives you a solid picture of how to set up routes for a RESTful API in Express.
Express exposes a Router object that lets you define detailed routing, where the method names on the Router match the HTTP method names. Since we’ve got a router object, we could create another instance and mount it on a different endpoint. Notice app.use('/api/', router) mounts our router at /api, so all endpoints live under http://domain.com/api/departments.
HTTP Semantics
We’ve established that we’re using HTTP. That means we should follow the HTTP specification, including proper status codes.
HTTP GET methods should use 200 (OK) and 404 (Not Found). Resource found? Return it with 200. Not found? Return 404.
HTTP POST methods should use 201 (Created) or 400 (Bad Request). A POST sends a payload that creates a resource. If it succeeds, return 201. If the client sends bad data, return 400.
HTTP PUT has two success paths. If the request creates a new resource, return 201 (Created). If it updates an existing one, return 204 (No Content) or 200. If the update can’t be applied, return 409 (Conflict).
For HTTP PATCH, a malformed patch document earns a 400. Valid patch data that can’t be applied gets a 409. A successful patch returns 204.
HTTP DELETE is binary: resource found and deleted? Return 204. Not found? Return 404.
Other status codes worth knowing:
401(Unauthorised) - client tries to access something that needs authentication.403(Forbidden) - valid request, but the server refuses it. Usually a permissions issue (like trying to access a file you can’t view).500(Internal Server Error) - something broke on the server. Details unknown.503(Service Unavailable) - server’s down, either from an error or heavy load.
HTTP status codes fall into five categories:
- 1xx - Informational messages
- 2xx - Success messages
- 3xx - Redirection messages
- 4xx - Client error messages
- 5xx - Server error messages
HTTP Status codes and Express
Express lets you set HTTP status codes in route handler functions:
const handlerFn = function (req, res) {
const id = +req.params.id;
if (id) {
// lookup the department information
return res.status(200).json(deptInfo);
} else {
return res.status(404).json(`Department with ${id} not found.`);
}
};
router.get('/departments/:id', handlerFn);
Parameters in REST
Sometimes you only need part of a resource. In our HR system, maybe we just want department names and locations, not the full record. And when fetching all employees, that dataset could be enormous.
REST APIs should support filtering and pagination.
Filtering lets you specify which fields to return and which entities match certain conditions. For large datasets, you need to control how many records come back.
There’s an ongoing debate about whether data processing should happen on the server or the client. Consider the employee list: fetching every employee and letting the browser filter them (say, by salary) wastes bandwidth and processing power. Send the filtered data from the server. It’ll be a much smaller payload.
Parameters in Express
Express supports query parameters, so you can build endpoints like: http://domain.com/employees?limit=10&fields=name,salary&sort=salary&sortType=desc
That returns the name and salary of the ten highest-earning employees.
Express exposes req.query to capture these parameters:
const handlerFn = function (req, res) {
if (req.query) {
const limit = req.query.limit;
const fields = req.query.fields;
const sort = req.query.sort;
const sortType = req.query.sortType;
// only a conceptual implementation
const filteredEmployees = `SELECT ${fields} FROM EMPLOYEES ORDER BY ${sort} ${sortType} LIMIT ${limit}`.exeute();
return res.status(200).json(filteredEmployees);
} else {
return res.status(200).json(employees);
}
};
router.get('/employees', handlerFn);
Versioning
Larger APIs need versioning planned upfront. Breaking changes will come. How do you handle them? Three common approaches exist: URI versioning, query string versioning, and header versioning.
URI versioning
The simplest to understand. URIs stay the same but get a version number appended.
http://domain.com/departments
becomes
http://domain.com/v2/departments
In Express, the Router object makes this easy. Mount new routes on new endpoints:
app.use('/api', router);
app.use('/v2/api', newRouter);
Query string versioning
Append a version query string to the URI.
http://domain.com/departments
becomes
http://domain.com/departments?v=2
In Express, capture this with req.query.
Header versioning
Nothing stops you from adding custom headers with custom values to requests and responses. Version information fits nicely in a custom header:

That’s how it looks in Insomnia.
Capture custom headers in Express and act on them:
const handlerFn = function (req, res) {
const versionHeader = req.headers['custom-version-header'];
if (versionHeader === 'version-1') {
// do version-1 action
} else {
// do version-2 action
}
};
router.get('/departments', handlerFn);
HATEOAS
HATEOAS (Hypertext As The Engine Of Application State) lets clients navigate between resources easily. Earlier we said that resource relationships could be represented via URI structure like /departments/12/employees, but deep nesting gets messy fast.
A better approach: embed links inside resources. Full URI representations sit inside the returned entities, letting clients navigate to related resources without guessing URLs.
At the time of writing, no standards exist for how HATEOAS should work.
Here’s an example implementation:
{
"departmentID": 12,
"departmentName": "Sales",
"departmentLocation": "London, UK",
"links": [
{
"rel": "employee",
"href": "http://domain.com/employee/1268",
"action": "GET"
},
{
"rel": "employee",
"href": "http://domain.com/employee/1891",
"action": "GET"
},
{
"rel": "self",
"href": "http://domain.com/department/12",
"action": "POST"
},
{
"rel": "self",
"href": "http://domain.com/department/12",
"action": "DELETE"
}
]
}
We’ve retrieved department info, and we get an array of links. Some point to related resources (employees at this department, like /department/12/employees). Others point to self, meaning we can invoke different HTTP methods on the same resource.
One API that implements a similar mechanism is the Star Wars API at https://swapi.co.
GraphQL
Worth mentioning GraphQL here. Check the Introduction to GraphQL post for details. The short version: REST and GraphQL don’t compete. They complement each other.
Summary
We covered the basics of REST API design. There’s still more ground to cover (caching, authentication, and other essentials), but this should give you a solid foundation for understanding and building RESTful APIs.