Planning ahead to ensure your software can evolve along with the rest of the tech world is even more essential when you’re developing an API. If you don’t have a plan for versioning your API, you could find yourself up a creek without a paddle. And it’ll take a lot of time, cough, money, to get the heck out of that creek.
In this post, we’ll consider some best practices for versioning an API, no matter what, and then go on to discuss three possible ways to architect your API, depending on your project and situation.
Once an API is published, it’s frozen. You can’t change that original code, or you’ll mess everyone up who has plugged into the API. So you create versions, build the right tests to give you confidence that the versions will work, and live by RAD (Rapid Application Development) so that you don’t spin too many wheels before seeing human interactions with your code.
For the purpose of this post, we’ll assume you’ve built your app with the following layers:
- Router: When a request comes in it hits the router
- Resources (a.k.a. views): A data entity that is exposed through your APIs
- Controllers: Sit above models
- Models: Lowest layer
Identifying your Version
No matter how you go about versioning your API, ensure it’s clear to developers which API they’re working in. Here we explore four options:
Include version in the URL
This is perhaps the most common method I have seen in the wild. If you include the version information in the URL, then your application’s router needs to parse the version from the requested URL and decide which code executes.
This method only applies to inbound requests made to your application. If you application uses webhooks or other outbound requests, then it might not make sense to include the version in the outgoing URL, particularly since you might not control the application on the other side.
Define a custom header
All HTTP requests and responses come with headers. If you have control over both the client and the server, you can send version information back and forth using these headers.
For example, a requesting client can make a request with a header indicating which version is desired. The server can then consider that information and send back the appropriate response with a header describing the version delivered.
Define versioned media types
An observant reader might have noticed that the custom headers solution above bears a striking similarity to an existing feature in the HTTP spec. Namely, content negotiation.
If you define versioned media types (for example, “application/vnd.com.example.foo.v1”), then clients can use the “Accept” header to express their desired version. Servers in turn can use the “Content-Type” header to describe the version of the response.
Use a flexible media type that doesn’t need to be versioned
HTML has been around in one form or another for around 20 years. In that time, clients and servers have simply used the media type “text/html.” It is well worth asking how HTML manages to continue to evolve without using any explicit version information. With a hypermedia API that’s written well enough you may not even need to version your API.
Three Ways to Architect Your API to Handle Versioned Requests
Whenever you version an API, you have to make architectural decisions that involve tradeoffs between duplication and flexibility. Below, we cover three different ways you can version APIs on that spectrum:
1. Versioning proxy, which points requests to versioned apps
This structure gives you the most flexibility but also requires the most duplication. To build a versioning proxy, keep one version static, and change as much as you want on the new version. Basically, you’re starting from scratch on your second version.
- You can start from scratch if your version 1 code wasn’t up to snuff.
- You have the freedom to transition to a new architecture.
- You’ll be able to sleep at night if your tests don’t give you enough coverage, and you are worried that development on version 2 could affect version 1 (which could happen if you share a model).
- You’ll spend a lot of time on duplication.
- You can kiss your cash goodbye, because this is the most expensive and time consuming way to structure an API.
How to do it
Two ways of routing requests to two different apps
Application.routes.draw do scope :v1 do mount ApiV1.new # /v1/whatever end scope :v2 do mount ApiV2.new # /v2/whatever end end
Application.router.draw do constraints(:version => 1) do mount ApiV1.new end constraints(:version => 2) do mount ApiV2.new end end
2. One router, which points requests to versioned controllers
With this model, you can reuse your code from version one, and don’t have to rewrite all of it for version 2. This structure can work if your models stay the same, but the controllers change (for example, different authentication schemes). You need good separation between models and resources for this route–both conceptually and in the code.
- You’ll be able to avoid duplication. For example, data persistence and complicated code can probably stay the same–only relevant parts have to change.
- You’ll save time, because you don’t have to fix bugs in two different places.
- You could accidentally change version 1 of your API, which could cause major problems for developers who are relying on it.
- You absolutely must have good test coverage. Or you will be losing sleep.
How to do it
Two ways to point one router to two different controllers
Applications.routes.draw do namespace :v1 do resources :orders # V1::OrdersController, /v1/orders end namespace :v2 do resources :orders # V2::OrdersController, /v2/orders end end
Application.router.draw do constraints(:version => 1) do # namespace V1 resources :orders # V1::OrdersController, /orders end constraints(:version => 2) do # namespace V2 resources :orders # V2::OrdersController, /orders end end
3. One router, shared controllers, which respond with versioned representations
While the others work, this is our ideal architecture for building APIs, because you’ve taken the changes between version 1 and version 2 and isolated them into the smallest unit possible. However, this method does requires a separation between models and resources.
Here’s what this would look like: if you’re a shipping center and you used to need to pack and ship orders back and forth from one warehouse but need to expand to two, you now have more information that needs to go into the orders resource. To do this, you’d create a new representation to include this resource. So you’d have one resource, but a representation for both version 1 and version 2.
- You’ll be doing the least duplication with this method, because you can share apps, models, controllers, routes, and a lot of resources.
- You’ll reduce the burden of maintenance and creating new versions with this version because all you have to do is swap a representation to make a change. This is good if you’re in a fast-moving industry.
- You’ll inherit the code from your v1 API. So if it’s messy, you won’t be able to fix it in this version, or future versions built like this.
- You need to, gasp, plan ahead. Achieve this by having a good separation of the different layers from the get-go. Separate models from resources, separate resources from their representation.
- You’ll need extremely good testing of all of your API versions. Because this method involves the most sharing, it makes it the most likely that a change in one place is going to actually affect users. Automated tests will make sure that you don’t inadvertently change something from another version.
Here’s a controller, in rails, having multiple representations:
class ResourceController responds_to :api_v1, :api_v2 def show data = Data.find(params[:id]) # data should have #to_api_v1 and #to_api_v2 methods respond_with(data) end end class ResourceController def show data = Data.find(params[:id]) data.extend(current_version.const_get(“DataRepresenter”)) # mixed-in representer defines as_json render :json => data end end
class ResourceController def show data = Data.find(params[:id]) presenter = current_version.const_get(“DataPresenter”)) render :json => presenter.new(data) end end * rails custom mime types
In the above example, we used Ruby’s “constant lookup” via const_get to find the appropriate presenter for our data. This pattern should apply to any of the methods described in “Identifying your version,” above.
However, if you are using media types, here is a simple way to define current_version:
In an initializer, register with Rails the media types you plan to use:
Mime::Type.register "application/vnd.com.example.v1+json", :api_v1 Mime::Type.register "application/vnd.com.example.v2+json", :api_v2
In your application’s controller, define a mapping between versions and the modules containing version-specific code.
class ApplicationController < ActionController::Base ... def current_version case request.format when Mime[:api_v1] ApiV1 when Mime[:api_v2] ApiV2 else # you can specify a default or return an error code end end ... end
What tips do you have for API versioning? Share them in the comments.