job.answiz.com
  • 4
Votes
name

Trying to design an API for external applications with foresight for change isn't easy, but a little thought up front can make life easier later on. I'm trying to establish a scheme that will support future changes while remaining backward compatible by leaving prior version handlers in place.

The primary concern on this article is to what pattern should be followed for all defined endpoints for a given product/company.

Base Scheme

Given a base URL template of https://rest.product.com/ I have devised that all services reside under /api along with /auth and other non-rest based endpoints such as /doc. Therefore I can establish the base endpoints as follows:

https://rest.product.com/api/...
https://rest.product.com/auth/login
https://rest.product.com/auth/logout
https://rest.product.com/doc/...

Service Endpoints

Now for the endpoints themselves. Concern about POST,GET,DELETE is not the primary objective of this article and is the concern on those actions themselves.

Endpoints can be broken down into namespaces and actions. Each action must also present itself in a way to support fundamental changes in return type or required parameters.

Taking a hypothetical chat service where registered users can send messages we may have the following endpoints:

https://rest.product.com/api/messages/list/{user}
https://rest.product.com/api/messages/send

Now to add version support for future API changes which may be breaking. We could either add the version signature after /api/ or after /messages/. Given the send endpoint we could then have the following for v1.

https://rest.product.com/api/v1/messages/send
https://rest.product.com/api/messages/v1/send

So my first question is, what is a recommended place for the version identifier?

Managing Controller Code

So now we have established we need to support prior versions we need to thus somehow handle code for each of the new versions which may deprecate over time. Assuming we are writing endpoints in Java we could manage this through packages.

package com.product.messages.v1;
public interface MessageController {
    void send();
    Message[] list();
}

This has the advantage that all code has been separated through namespaces where any breaking change would mean that a new copy of the service endpoints. The detriment of this is that all code needs to be copied and bug fixes wished to be applied to new and prior versions needs to be applied/tested for each copy.

Another approach is to create handlers for each endpoint.

package com.product.messages;
public class MessageServiceImpl {
    public void send(String version) {
        getMessageSender(version).send();
    }
    // Assume we have a List of senders in order of newest to oldest.
    private MessageSender getMessageSender(String version) {
        for (MessageSender s : senders) {
            if (s.supportsVersion(version)) {
                return s;
            }
        }
    }
}

This now isolates versioning to each endpoint itself and makes bug fixes back port compatible by in most cases only needing to be applied once, but it does mean that we need to do a fair bit more work to each individual endpoint to support this.

So there's my second question "What's the best way to design REST service code to support prior versions."

We need to make sure that the base URL of the API is simple. For example, if we want to design APIs for products, it should be designed like —

/products
/products/12345

The first API is to get all products and the second one is to get specific product.

Use nouns and NOT the verbs

A lot of developers make this mistake. They generally forget that we have HTTP methods with us to describe the APIs better and end up using verbs in the API URLs. For instance, API to get all products should be

/products

and NOT as shown below

/getAllProducts

Some common URL patterns, I have seen so far

Use of right HTTP methods

RESTful APIs have various methods to indicate the type of operation we are going to perform with this API —

  • GET — To get a resource or collection of resources.
  • POST — To create a resource or collection of resources.
  • PUT/PATCH — To update the existing resource or collection of resources.
  • DELETE — To delete the existing resource or the collection of resources.

We need to make sure we use the right HTTP method for given operation.

Use Plurals

This topic is bit debatable. Some of people like to keep the resource URL with plural names while others like to keep it singular. For instance —

/products
/product

I like to keep it plural since it avoids confusion whether we are talking about getting single resource or a collection. It also avoids adding additional things like attaching all to the base URL e.g. /product/all

Some people might not like this but my only suggestion is to keep is uniform across the project.

Use parameters

Sometime we need to have an API which should be telling more story than just by id. Here we should make use of query parameters to design the API.

  • /products?name=’ABC’ should be preffered over/getProductsByName
  • /products?type=’xyz’ should be preferred over /getProductsByType

This way you can avoid long URLs with simplicity in design.

Use proper HTTP codes

We have plenty of HTTP codes. Most of us only end up using two — 200 and 500! This is certainly not a good practice. Following are some commonly used HTTP codes.

  • 200 OK — This is most commonly used HTTP code to show that the operation performed is successful.
  • 201 CREATED — This can be used when you use POST method to create a new resource.
  • 202 ACCEPTED — This can be used to acknowledge the request sent to the server.
  • 400 BAD REQUEST — This can be used when client side input validation fails.
  • 401 UNAUTHORIZED / 403 FORBIDDEN— This can be used if the user or the system is not authorised to perform certain operation.
  • 404 NOT FOUND— This can be used if you are looking for certain resource and it is not available in the system.
  • 500 INTERNAL SERVER ERROR — This should never be thrown explicitly but might occur if the system fails.
  • 502 BAD GATEWAY — This can be used if server received an invalid response from the upstream server.

Versioning

Versioning of APIs is very important. Many different companies use versions in different ways, some use versions as dates while some use versions as query parameters. I generally like to keep it prefixed to the resource. For instance —

/v1/products
/v2/products

I would also like to avoid using /v1.2/products as it implies the API would be frequently changing. Also dots (.) might not be easily visible in the URLs. So keep it simple.

It is always good practice to keep backward compatibility so that if you change the API version, consumers get enough time to move to the next version.

Use Pagination

Use of pagination is a must when you expose an API which might return huge data and if proper load balancing is not done, the a consumer might end up bringing down the service.

We need to always keep in mind that the API design should be full proof and fool proof.

Use of limit and offset is recommended here. For example, /products?limit=25&offset=50 It is also advised to keep a default limit and default offset.

Supported Formats

If is also important to choose how your API responds. Most of the modern day applications should return JSON responses unless you have an legacy app which still needs to get XML response.

Use Proper Error Messages

It is always a good practice to keep set of error messages application sends and respond that with proper id. For example, if you use Facebook graph APIs, in case of errors, it returns message like this —

{
  "error": {
    "message": "(#803) Some of the aliases you requested do not exist: products",
    "type": "OAuthException",
    "code": 803,
    "fbtrace_id": "FOXX2AhLh80"
  }
}

I have also seen some examples in which people return URL with error message which tells you more about the error message and how to handle it as well.

Use of Open API specifications

In order to keep all teams in your company abide to certain principles, use of OpenAPI Specification can be useful. Open API allows you to design your APIs first and share that with the consumers in easier manner.

Conclusion

It is quite evident that if you want to communicate better, APIs are the way to go. But if they are designed badly then it might increase confusion. So put best efforts to design well and rest is just the implementation.

  • 0
Reply Report

Another approach to handle API versioning is to use Version in HTTP Headers. Like

POST /messages/list/{user} HTTP/1.1
Host: http://rest.service.com
Content-Type: application/json
API-Version: 1.0      <----- like here
Cache-Control: no-cache

You can parse the header and handle it appropriately in the backend.

With this approach the clients need not to change the URL, just the header. And also this makes the REST endpoints cleaner, always.

If any of the client didn't send the version header, you either send 400 - Bad request or you can handle it with backward-compatible version of your API.

  • 0
Reply Report

So there's my second question "What's the best way to design REST service code to support prior versions."

A very carefully designed, and orthogonal API will probably never need to be changed in backward incompatible ways, so really the best way is not to have future versions.

Of course, you probably won't really get that the first try; So:

Version your API, just as you are planning (and it's the API that is versioned, not individual methods within), and make lots of noise about it. Make sure that your partners know that the API can change, and that their applications should check to see if they are using the latest version; and advise users to upgrade when a newer one is available. Supporting two old versions is tough, supporting five is untenable.
Resist the urge to update the API version with every "release". The new features can usually be rolled into the current version without breaking existing clients; they're new features. The last thing you want is for clients to ignore the version number, since it's mostly backwards compatible anyway. Only update the API version when you absolutely cannot move forward without breaking the existing API.
When the time comes to create a new version, the very first client should be the backwards-compatible implementation of the previous version. The "maintenance API" should itself be implemented on the current API. This way you aren't on the hook for mantaining several full implementations; only the current version, and several "shells" for the old versions. Running a regression test for the now deprecated API against the backwards comaptible client is a good way to test out both the new API and the compatibility layer.
  • 2
Reply Report