Building Better REST APIs: A Hybrid Approach with JSend and JSON:API Guidelines
This hybrid API design guide combines elements from both JSend and JSON:API to create a powerful and user-friendly approach for developing REST APIs. By adopting the best practices from both, this guide offers a consistent and maintainable API design.
Versioning
API versioning can be achieved using a variety of methods such as URI-based route prefix, version in the header of the request, or as a query string value.
The most common approach is using a URI-based route prefix for RESTful routes to define versions. This approach requires including the API version identifier as a part of the URL path, preferably as the first segment. For example, the API endpoint URL could look like:
Example:
http://api.coderrob.com/v1/projects/directory-viewer
This approach helps maintain backward compatibility and ensures that clients are aware of the API version they are interacting with. It also allows for clear separation of different versions of the API and simplifies the process of updating or deprecating specific versions.
While the route-based approach for versioning has its benefits, it also has some downsides…
One significant issue is that it can be more challenging to reflect updates to individual routing endpoints without modifying the explicit routes. As a result, it can lead to hard-coding, making maintenance more complicated, and potentially creating confusion among developers trying to consume multiple versions in parallel.
My Recommendation: Query String + Semantic Versioning
A more flexible approach that addresses these issues is to use semantic versioning through the query string. This approach involves adding a “version” parameter to the API endpoint URL’s query string.
For example, the API endpoint URL could look like:
https://api.coderrob.com/projects/directory-viewer?version=1.0.0
This approach allows for:
Supporting individual endpoint versioning without requiring explicit route redirects or custom route rules to be updated, making it more flexible and easier to maintain.
Avoiding potential issues with proxy rules that may cutoff version defined headers.
Enabling developers to make changes without hard-coding the version in the URL, making it easier to update the API while still maintaining backward compatibility.
Overall, while there are benefits to using a route-based approach, query string semantic versioning is a more flexible and practical solution for many use cases.
Response Contract
A standard HTTP response contract should be used for all returned data from the API. This API response contract provides a common set of properties and data consistency from endpoint to endpoint.
The response contract includes properties like data
, status
, errors
, links
, and trackingId
. The data property can contain singleton resources or resource collections, while the errors property offers information about any encountered issues during the request.
Property | Required | Description |
---|---|---|
data | Y | The response’s primary data. For singleton resource the data will be a simple value type or object. For resource collections, data will be an array of objects. |
status | Y | The overall HTTP status code of the response. Useful in situations where the HTTP response is HTTP status is inaccessible. |
errors | For any encountered errors, one or more error objects will be returned in the response. These error objects provide additional information for problems encountered in the request. | |
links | To support pagination, the links object can contain cursor-based or offset-based properties. Useful in situations where direct HTTP Link headers (RFC8288) are inaccessible. | |
trackingId | A unique request tracking Id used for auditing or troubleshooting purposes. |
Data Property
Singleton Resource
A simple GET request to a singleton response should return an object or simple value.
curl https://api.coderrob.com/v1/orders/287f34e5-8439-491b-a2b0-112804100fa1
HTTP/1.1 200 OK
{
"status": 200,
"data": {
"id" : "287f34e5-8439-491b-a2b0-112804100fa1",
}
}
If there is no data to return but the resource is a known entity, the data property should be null
.
HTTP/1.1 200 OK
{
"status": 200,
"data": null
}
If data is not found and the resource is unknown, the expected response would be an HTTP 404
status with null
data.
curl https://api.coderrob.com/v1/orders/287f34e5-8439-491b-a2b0-112804100fa1
HTTP/1.1 404 Not Found
{
"status": 404,
"data": null
}
Response Collections
A simple GET request to a collection of responses should return an array of objects or values.
curl https://api.coderrob.com/v1/orders
HTTP/1.1 200 OK
{
"status": 200,
"data": [
{
"id": "287f34e5-8439-491b-a2b0-112804100fa1",
. . .
}
]
}
If there is no data to return, but the resource is a known entity, the data property should be an empty array.
HTTP/1.1 200 OK
{
"status": 200,
"data": []
}
If data is not found and the resource is unknown, the expected response would be an HTTP 404
status with an empty array of data.
curl https://api.coderrob.com/v1/orders
HTTP/1.1 404 Not Found
{
"status": 404,
"data": []
}
Errors Property
The errors property contains a collection of error objects and is only present in cases of service failures, conflict identification, or when providing additional information about backend problems.
Each error object should have properties like id
, status
, code
, name
, and description
to help clients identify and correct issues.
Property | Required | Description |
---|---|---|
id | A unique Id of the specific error. | |
status | The HTTP status code of any dependent services or server processes that resulted in errors or failures. | |
code | Y | A string value representing any product or service-specific error codes. |
name | A brief consistent name for the type of That’error. | |
description | A brief human readable description of the error. |
During development, it’s essential to be cautious about exposing too much information to avoid potential security threats.
For example, let’s look at a POST response to a request that was published to a backend, but the data failed some business requirements.
In this case we received a HTTP 400
status code, and the response indicates there are errors related to missing data needed for the backend business logic.
HTTP/1.1 400 Bad Request
{
"status": 400,
"data": null,
"errors": [
{
"name": "PostalCodeRequired",
"code": "100723",
"description": "Postal code is required."
},
{
"name": "EmailRequired",
"code": "100701",
"description": "Email address is required."
}
],
"trackingId": "OHNOZ_123810-12387"
}
Using unique error codes or human readable messaging, the error objects provide additional information to the caller that can be used for building more robust API integrations.
I’m particularly keen to avoid coupling between systems. To understand why, please check out my post, “Decoupling UI Messaging From Backend Error Codes in Web Applications” to learn more.
Links Property
The links object can be customized based on the pagination strategy adopted by the API. There are cursor
, page
, or offset-based
pagination strategies that provide links to itself or additional resources.
For instance, a page
-based pagination strategy would likely include properties such as self
, first
, last
, prev
, and next
.
Note: This would be similar to the navigational structure of the Links HTTP header.
Property | Description |
---|---|
self | The link of the current page of data. |
first | The link to the first page of data. |
last | The link to the last page of data. |
prev | The link to the previous page of data. |
next | The link to the next page of data. |
A paged response could look similar to the following example response:
HTTP/1.1 200 OK
{
"status": 200,
"data": [
{
"id": "287f34e5-8439-491b-a2b0-112804100fa1",
}
],
"links": {
"self": "https://blog.coderrob.com/posts?offset=1&limit=1",
"first": "https://blog.coderrob.com/posts?offset=0&limit=1",
"last": "https://blog.coderrob.com/posts?offset=10&limit=1",
"prev": "https://blog.coderrob.com/posts?offset=0&limit=1",
"next": "https://blog.coderrob.com/posts?offset=2&limit=1",
},
"trackingId": "ICANHASPOSTS_123810-12388"
}
TrackingId Property
The trackingId property is a unique request tracking ID used for auditing or troubleshooting purposes. It can be included in the response to help developers identify specific requests when debugging issues.
HTTP/1.1 200 OK
{
"status": 200,
"data": [],
"trackingId": "IHASAUDITING_123810-12389"
}
Implementing the Hybrid Approach
By adopting the hybrid approach, combining elements from JSend and JSON:API guidelines, you can design a consistent, maintainable, and user-friendly API. Remember to consider the following best practices:
- Use URI-based, query string, or header based route versioning.
- Implement a standard HTTP response contract with properties like data, status, errors, links, and trackingId.
- Handle singleton resources and response collections appropriately.
- Provide detailed error objects with relevant information without exposing sensitive data.
- Customize the links object according to a desired pagination strategy.
With these best practices in mind, you can create a robust API that offers a smooth experience for developers and ensures long-term maintainability.