API Versioning: 3 Ways to Architect Your API to Handle Versioned Requests

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.

Why Version?

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.

App Layers

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.

The pros:

  • 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).

The cons:

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

The pros:

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

The cons:

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

The pros:

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

The cons:

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

Check out our other articles on API:

What makes a good API?

API Planning and Proceeding: Tell Me What You're Working With

API Development: Turning Controller Actions Into Services

__

Image Source