REST APIs are one of the most common kinds of web interfaces available today. They allow various clients including browser apps to communicate with services via the REST API. Therefore, it's very important to design REST APIs properly so that we won't run into problems down the road. We have to take into account security , performance, and ease of use for API consumers.
Otherwise, we create problems for clients that use our APIs, which isn’t pleasant and detracts people from using our API. If we don’t follow commonly accepted conventions, then we confuse the maintainers of the API and the clients that use them since it’s different from what everyone expects.
In this article, we'll look at how to design REST APIs to be easy to understand for anyone consuming them, future-proof , and secure and fast since they serve data to clients that may be confidential.
- Accept and respond with JSON
- Use nouns instead of verbs in endpoint paths
- Name collections with plural nouns
- Nesting resources for hierarchical objects
- Handle errors gracefully and return standard error codes
- Allow filtering, sorting, and pagination
- Maintain Good Security Practices
- Cache data to improve performance
- Versioning our APIs
best practices for REST API authentication .
some people think REST should only return hypertext (including Roy Fielding who created the term) REST APIs should accept JSON for request payload and also send responses to JSON. JSON is the standard for transferring data. Almost every networked technology can use it: JavaScript has built-in methods to encode and decode JSON either through the Fetch API or another HTTP client. Server-side technologies have libraries that can decode JSON without doing much work.
There are other ways to transfer data. XML isn’t widely supported by frameworks without transforming the data ourselves to something that can be used, and that’s usually JSON. We can’t manipulate this data as easily on the client-side, especially in browsers. It ends up being a lot of extra work just to do normal data transfer.
Form data is good for sending data, especially if we want to send files. But for text and numbers, we don’t need form data to transfer those since—with most frameworks—we can transfer JSON by just getting the data from it directly on the client side. It’s by far the most straightforward to do so.
To make sure that when our REST API app responds with JSON that clients interpret it as such, we should set
Content-Type
in the response header to
application/json
after the request is made. Many server-side app frameworks set the response header automatically. Some HTTP clients look at the
Content-Type
response header and parse the data according to that format.
The only exception is if we’re trying to send and receive files between client and server. Then we need to handle file responses and send form data from client to server. But that is a topic for another time.
We should also make sure that our endpoints return JSON as a response. Many server-side frameworks have this as a built-in feature.
Let’s take a look at an example API that accepts JSON payloads. This example will use the
Express
back end framework for Node.js. We can use the
body-parser
middleware
to parse the JSON request body, and then we can call the
res.json
method with the object that we want to return as the JSON response as follows:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.post('/', (req, res) => {
res.json(req.body);
app.listen(3000, () => console.log('server started'));
bodyParser.json()
parses the JSON request body string into a JavaScript object and then assigns it to the
req.body
object.
Set the
Content-Type
header in the response to
application/json; charset=utf-8
without any changes. The method above applies to most other back end frameworks.
CRUD operations.
With the two principles we discussed above in mind, we should create routes like GET
/articles/
for getting news articles. Likewise, POST
/articles/
is for adding a new article , PUT
/articles/:id
is for updating the article with the given
id
. DELETE
/articles/:id
is for deleting an existing article with the given ID.
/articles
represents a REST API resource. For instance, we can use Express to add the following endpoints for manipulate articles as follows:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.get('/articles', (req, res) => {
const articles = [];
// code to retrieve an article...
res.json(articles);
app.post('/articles', (req, res) => {
// code to add a new article...
res.json(req.body);
app.put('/articles/:id', (req, res) => {
const { id } = req.params;
// code to update an article...
res.json(req.body);
app.delete('/articles/:id', (req, res) => {
const { id } = req.params;
// code to delete an article...
res.json({ deleted: id });
app.listen(3000, () => console.log('server started'));
In the code above, we defined the endpoints to manipulate articles. As we can see, the path names do not have any verbs in them. All we have are nouns. The verbs are in the HTTP verbs.
The POST, PUT, and DELETE endpoints all take JSON as the request body, and they all return JSON as the response, including the GET endpoint.
- Bad Gateway - This indicates an invalid response from an upstream server.
- 503 Service Unavailable - This indicates that something unexpected happened on server side (It can be anything like server overload, some parts of the system failed, etc.).
We should be throwing errors that correspond to the problem that our app has encountered. For example, if we want to reject the data from the request payload, then we should return a 400 response as follows in an Express API:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
// existing users
const users = [
{ email: 'abc@foo.com' }
app.use(bodyParser.json());
app.post('/users', (req, res) => {
const { email } = req.body;
const userExists = users.find(u => u.email === email);
if (userExists) {
return res.status(400).json({ error: 'User already exists' })
res.json(req.body);
app.listen(3000, () => console.log('server started'));
In the code above, we have a list of existing users in the
users
array with the given email.
Then if we try to submit the payload with the
email
value that already exists in
users
, we'll get a 400 response status code with a
'User already exists'
message to let users know that the user already exists. With that information, the user can correct the action by changing the email to something that doesn't exist.
Error codes need to have messages accompanied with them so that the maintainers have enough information to troubleshoot the issue, but attackers can’t use the error content to carry our attacks like stealing information or bringing down the system.
Whenever our API does not successfully complete, we should fail gracefully by sending an error with information to help users make corrective action.
Redis , in-memory caching, and more. We can change the way data is cached as our needs change.
For instance, Express has the
apicache
middleware to add caching to our app without much configuration. We can add a simple in-memory cache into our server like so:
const express = require('express');
const bodyParser = require('body-parser');
const apicache = require('apicache');
const app = express();
let cache = apicache.middleware;
app.use(cache('5 minutes'));
// employees data in a database
const employees = [
{ firstName: 'Jane', lastName: 'Smith', age: 20 },
//...
{ firstName: 'John', lastName: 'Smith', age: 30 },
{ firstName: 'Mary', lastName: 'Green', age: 50 },
app.use(bodyParser.json());
app.get('/employees', (req, res) => {
res.json(employees);
app.listen(3000, () => console.log('server started'));
The code above just references the
apicache
middleware with
apicache.middleware
and then we have:
app.use(cache('5 minutes'))
to apply the caching to the whole app. We cache the results for five minutes, for example. We can adjust this for our needs.
If you are using caching, you should also include
Cache-Control
information in your headers. This will help users effectively use your caching system.