Coderrob brand logo Coderrob

Hi, I'm Rob. I'm a programmer, Pluralsight author, software architect, emerging technologist, and life long learner.


Building Better REST APIs: A Hybrid Approach with JSend and JSON:API Guidelines


Mon, 01 May 2023

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:

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.

PropertyRequiredDescription
dataYThe 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.
statusYThe overall HTTP status code of the response. Useful in situations where the HTTP response is HTTP status is inaccessible.
errorsFor 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.
linksTo support pagination, the links object can contain cursor-based or offset-based properties. Useful in situations where direct HTTP Link headers (RFC8288) are inaccessible.
trackingIdA 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.

PropertyRequiredDescription
idA unique Id of the specific error.
statusThe HTTP status code of any dependent services or server processes that resulted in errors or failures.
codeYA string value representing any product or service-specific error codes.
nameA brief consistent name for the type of That’error.
descriptionA 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.

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.

PropertyDescription
selfThe link of the current page of data.
firstThe link to the first page of data.
lastThe link to the last page of data.
prevThe link to the previous page of data.
nextThe 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:

With these best practices in mind, you can create a robust API that offers a smooth experience for developers and ensures long-term maintainability.