REST API Best Practices for URL Paths and Versioning
REST API Best Practices for URL Paths and Versioning
A REST API (Representational State Transfer API) is an architectural style for building APIs. It is also known as RESTful API and is widely used for system integrations and web client-server communication.
Therefore, with its wide adoption, several best practices emerged to standardize REST. Out of them, URL path and versioning conventions take precedence.
Here are the practices you need to follow for URL paths and versioning when implementing REST APIs.
1. Only use nouns for URL paths
Following a standard convention for URL paths is essential to understand the use of that API. The HTTP method (GET, POST, DELETE and PUT) typically covers the action you perform. So, it does not make any sense to write a GET request path like /getUsers
, since you already know it is a GET request.
Besides, if we use verbs for the paths, it makes it difficult to decide on which verb to use. For example, the URL paths /getUsers
, /fetchUsers
, /retrieveUsers
might convey the same meaning.
That's why you should always use nouns instead of verbs for URL paths.
# Correct URL Paths
GET - /users
GET - /users/:id
POST- /users
# Incorrect URL Paths
GET - /getUsers, /fetchUsers
POST - /saveUser
The below example shows a well-defined set of API path URLs using Node.js.
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.get('/users', (req, res) => {
// get all users
});
app.get('/users/:id', (req, res) => {
// get user by id
});
app.post('/users', (req, res) => {
// create new user
});
app.put('/users/:id', (req, res) => {
// update user by id
});
app.delete('/user/id', (req, res) => {
// delete user by id
});
2. Use nesting on URL paths
There are scenarios where you need to access an object inside another object. In such situations, you should design the API path URL to reflect the relationship between the 2 objects.
For example, assume that a single user can have multiple accounts, and you need to get account details. This can be addressed by appending the /accounts
path segment to /users
path.
# Correct URL Paths
GET - /users/:userId/accounts
GET - /users/:userId/accounts/:accountId
POST- /users/:userId/accounts
# Incorrect URL Paths
GET - /accounts
GET - /accounts/:accountId
POST - /accounts
Nesting URL paths like above may not be suitable for every situation. So, you must think about the relationships between the 2 objects and design the paths accordingly. For example, we can not use the nested URL paths above if every user does not have an account.
However, you should not use nested URL paths too deep. The maximum level of nesting you can use is 2 or 3 and exceeding that limit will make it hard to read and understand.
The below example shows a nested set of URL paths using Node.js.
const app = express();
app.use(bodyParser.json());
app.get('/users', (req, res) => {
// get all users
});
app.get('/users/:id', (req, res) => {
// get user by id
});
app.get('/users/:id/accounts', (req, res) => {
// get accounts by user id
});
app.get('/users/:id/accounts/:accountId', (req, res) => {
// get account user id and account id
});
app.post('/users', (req, res) => {
// create new user
});
app.post('/users/:id/accounts', (req, res) => {
// create new account
});
3. Do not reflect database structure
This is one of the most common mistakes developers make when designing API path URLs. They try to reflect the database relationships through the URLs by nesting. Although nesting allows defining URL paths more precise, you should not consider the database structure when designing them.
If you do so, there is a high risk of your application being vulnerable to attackers, since they can map your database schema, by looking at the URL paths.
4. Keep the base resource URL lean
Keeping the base URL path of APIs restricted to a single resource easily facilitates searching, sorting, and filtering requirements using query parameters.
For example, if you want to get a filtered set of users using the API, you can easily prepend query parameters to the base URL like below:
GET - /users?type=registered
As you can see, having a short, restricted base URL allows developers to understand the purpose of query parameters easily. Similarly, you can use query parameters for searching and sorting as well.
# Searching
GET - /users?name=Chameera
GET - /users?city=NewYork&country=America
# Sorting
GET - /users?name
GET - /users?name,age
5. Versioning APIs
Versioning is an essential part of API development since it allows to handle breaking changes. If there is a major change in your API, you can create a new version and keep the older version as it is without forcing every user or client to upgrade.
Most developers tend to use the URL for the versioning, while some argue it should be in the request header. However, there are 4 methods to version an API, and as a developer, you should be aware of these techniques to select the best one for your application.
Versioning through URL Path
Using the URL path is one of the most common ways to version an API. If you make a breaking change to the API, you can release the new version of your application by pointing to the latest APIs while keeping the old APIs for previous releases.
For example, you can include /v1/
for old API URLs and /v2/
for the latest API. Although some developers use the complete version number with major, minor, and the patch, it is recommended to only use the major.
# Recommended
GET - /v1/users
GET - /v2/users
# Not Recommended
GET - /1.0.0/users
GET - /1.2.3/users
app.get('/v1/users', (req, res) => {
// get all users
});
app.get('/v2/users', (req, res) => {
// get all users
});
Versioning through Query Parameters
This approach is pretty similar to the above method. Here, you can include your version as a query parameter in the API URL.
GET - /users?version=1
GET - /users?version=2
GET - /users?version=v1
GET - /users?version=v2
Versioning through Custom Headers
As mentioned, if you don't like to use versioning in the API URL path, you can use the request header to define the version. You can include the custom header Accept-Version
in the request with the version like below:
Accepts-version: 1.0
Accepts-version: 1.1.1
Accepts-version: 2.0
Versioning through Content Negotiation
The content negotiation method is pretty similar to the custom headers approach. But here, you can use the standard Accept
header instead of custom headers.
Accept: application/vnd.xm.device+json; version=1
Accept: application/vnd.v1+json
You can also combine these methods to version your API. For example, you can use the URL path versioning with the major and include the complete version in the request using custom headers. In fact, this approach is used in well-known products like Stripe.
API URL paths and versioning is import if you want to ensure that your APIs are high accessible and backward compatible. It also makes it easy for those consuming your APIs to differentiate between changes and make time provisions to support subsequent updates.