Design Patterns – Planning REST Endpoints for Future Changes

designdesign-patternsrestservicesweb services

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."

Best Answer

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.
Related Topic