Did you know that Netflix has hundreds of API versions, one for each device? Daniel Jacobson's Techniques for Scaling the Netflix API at QConSF 2011 explained why they chose this model. And while we don't all build distributed services that supply custom-tailored data to thousands of heterogeneous TVs and set-top boxes, we do have to pay close attention to API versioning from day one.

Versioning is hard. Your data models evolve, but you must maintain backward-compatibility for your public interfaces. While many strategies exist to deal with this problem, we'd like to propose one that requires very little programming effort and that is more declarative in nature.

At Artsy we use Grape and implement the "path" versioning strategy from the frontier branch. Our initial v1 API is consumed by our own website and services and lives at https://artsyapi.com/api/v1. We've also prototyped v2 and by the time v1 is frozen, it should already be in production.

Grape takes care of version-based routing and has a system that lets you split version-based presentation of a model from the model implementation. I find that separation forcefully induced by unnecessary implementation complexity around wanting to return different JSON depending on the API version requested. What if implementing versioning in as_json were super simple?

Consider a Person model returned from a v1 API.

1
2
3
4
5
6
7
8
9
class API < Grape::API
  prefix :api
  version :v1
  namespace :person
    get ":id"
      Person.find(params[:id]).as_json
    end
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
class Person
  include Mongoid::Document

  field :name

  def as_json
    {
      name: name
    }
  end

end

In v2 the model split :name into a :first and :last name and in v3 :name has finally been deprecated. A version v3 Person model would look as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person
  include Mongoid::Document

  field :first
  field :last

  def as_json
    {
      first: first,
      last: last
    }
  end

end

How can we combine these two implementations and write Person.find(params[:id]).as_json({ :version => ? })?

In mongoid-cached-json we've introduced a declarative way of versioning JSON. Here's the code for Person v3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person
  include Mongoid::Document
  include Mongoid::CachedJson

  field :first
  field :last

  def name
    [ first, last ].join(" ")
  end

  json_fields \
    name: { :versions => [ :v1, :v2 ] },
    first: { :versions => [ :v2, :v3 ] },
    last: { :versions => [ :v2, :v3 ] }

end

With the mongoid-cached-json gem you also get caching that respects JSON versioning, for free. Read about it here.

Categories: api, caching, mongoid, versioning


Comments