An API exists for third-parties to add value on top of your system. Versioning is a critical component of an API to prevent those third-party tools from breaking. I would never have thought API versioning could prove so controversial until I got into a few discussions on the subject this past week.
What does a RESTful API represent?
Before diving into the controversy, it's important to review what a RESTful API is intended to represent. If you don't agree with me on these points, you probably won't agree with me on the details.
A RESTful API provides access to representations of resources over standard HTTP requests. Each resource being requested is uniquely identifiable through a URI that should persist over time. If I want to refer to server #1234, it could be referenced in the API through the URI http://mycloud.example.com/server/1234. The underlying assumption is that once a client learns of this server object, it should feel free to reference that object through its URI from now and into the future. In other words, it persists over time as an way of identifying the resource between a client and the server.
I don't mean to suggest that the URI cannot change, but it should handle changes in accordance with the HTTP protocol. A 404 (NOT FOUND) response to a request for /server/1234 should be safely interpreted as the server no longer existing. A 302 (MOVED PERMANENTLY), on the other hand, flags a resource that is now referenced under a different URI. Clients should be able to safely make these assumptions.
The API is a representation of an underlying system that is distinct from the objects that make up the underlying system. The API returns an XML object that represents the server, but it does not provide any information about how a server might be represented internally within the system. The API may define a "server" XML entity though inside the cloud platform, multiple objects might represent that server concept. It's important to long-term system flexibility that the API and the underlying system be able to evolve independently.
The external representations (API, UI) focus on how third-parties consume information about the system. The internal representations focus on how the objects need to function to make the system work. The internal representation is a functional object. The API representation is JSON, XML, or some other document that describes that object. They are separate things.
Version negotiation is critical to the functioning of an API because it enables a client to indicate to the server what the client is expecting. The server can then handle the request and provide a response in a manner appropriate to the client's expectations.
Version negotiation traditionally works through one of four mechanisms:
- In the URI of the requested resource
- As a parameter to the query
- As a non-standard header
- A part of the content type
Including the version in the URI is a very bad thing. I say this having designed an API that initially included the version in the URI. A URI with the version in it looks like /v1/server/1234. The implication is that /v1/server/1234 is pointing to something distinct from /v2/server/1234.
Another approach is to add the version in as part of the query parameters, as in /server/1234?v=1. There's nothing technically wrong with this approach in that the canonical resource is still /server/1234. But there are better ways to handle the issue.
The approaches of using non-standard headers or the content type are both two sides to the same coin. They treat the requested resource as a single, persistent concept with multiple representations. These representations include content type and version. The client thus specifies the content type and version it wants and, if supported, the server responds in kind. From an elegance perspective, content type versioning can make more sense with simple APIs. It can, however, be complex to implement for richer APIs and requires you to parse the version information from the content type. I prefer a custom header, X-API-Version.
API management is one area in which programmatic laziness will kill you. Many programmers simply have libraries that automatically translate their internal representations of objects into JSON or XML for external representation. This laziness often works well for version 1 of the system, but it tightly couples the internal representation to the external representation. Whenever a change is made to the structure of the internal representation, the external representation is automatically changed.
The whole purpose of modular design and programming interfaces is to hide the internal representations of a system from external consumers. A good API therefore does not vary with model changes to the system, but instead only with functional changes to the system. Programmers building APIs do not use tools that automatically convert internal objects to external JSON or XML representations.
Another problem with tightly coupling the internal representation to the external representation is that the external representation is that it's hard to represent multiple versions of the external representation. Once you go to supporting more than just the current version of an API, you are committed to decoupling internal and external representation.
And you should commit to supporting more than just one version!
The reason you craft an API in the first place is to provide external tools with a clean interface into your system so that an ecosystem may flourish. When you make API changes without support for versioning, you are unilaterally committing your ecosystem to tie their update schedules to your development schedule. If some tool is interacting with your system and is not updated by the time of an upgrade, it will suddenly stop working.
Support for versioning guarantees that the server will accept requests using versions other than the latest version and provide responses aligned with the client request. Upgrades can thus be made to the core system without impacting client applications.
API Design Tips
- Negotiate content type and version through a custom header
- Do not use tools that automate the conversion of internal objects to external JSON or XML.
- Support all versions of the API so you don't suddenly break existing clients.