Republished from Opperator blog
Behind the Curtain posts explore the development of a piece of Opperator's architecture, including the what, why, and how.
Grape is a Ruby framework for building restful APIs on the web. We're using it extensively to build Opperator. Out of the box, Grape has built-in support for versioning your APIs. There's general information about how to use versioning in the Grape README and the wiki, but this post is more about digging into the nitty-gritty and discussing some of the design decisions and implementation details that've been merged in recently.
Previously, Grape supported API versioning by prefixing the version name in the url. For example:
class MyAPI < Grape::API version :v1 # GET /v1/cows get '/cows' do # retrieve bovine goodness end end
version is called, the version names are passed into
When a request comes in, the middleware looks for a matching version in the
path. If it finds the version, it rewrites the path info without the version
env[api.version], and moves along it's merry way. If no version
is matched, then a 404 is thrown.
This works as you'd expect, but introduces your versioning scheme into your resource uri's. Workable, but it messes up those pretty restful uris. Fortunately, the HTTP protocol Accept header is a perfect fit for this problem. RFC 2616 defines the Accept header as:
The Accept request-header field can be used to specify certain media types which are acceptable for the response. Accept headers can be used to indicate that the request is specifically limited to a small set of desired types, as in the case of a request for an in-line image.
While the example the RFC gives is related to multimedia, if you squint and replace the references to media with 'version', then you have a good overview of header based API versioning. This Accept header field can be used to scope a request to a specific API version. For example, the Github API understands the following Accept header:
The client who sent this header is asking the server "Hey Github, can you give me a responses that is version v1 and formatted in JSON?". When Github sees this request, it can do one of two things. If it's able to answer the question, then it processes the request as normal. However, if Github doesn't understand the Accept field value, then it should send a 406 Not Acceptable response.
Revisiting our code sample, we would define our API as follows:
class MyAPI < Grape::API version :v1, :using => :header, :vendor => 'intridea', :format => :json # GET /cows get '/cows' do # retrieve bovine goodness end end
Header based versioning is the new default versioning strategy, but I
explicitly specified it in the example for clarity. The
vendor option is new
and is a way to describe the vendor providing this API, and the
option is the expected response format. Similar to path based versioning,
Grape::Middleware is responsible for figuring out the version being
requested and setting
env[api.version]. But since there are now multiple
strategies for handling versions,
Grape::Middleware::Versioner has been
split into two middlewares.
Grape::Middleware::Versioner::Path is the
original path based middleware, and
the new kid on the block. Relevant commit
This new middleware will use the following format in the Accept header when matching for versions:
These are the fields that are original declared when
version was first
called. If the middleware is able to match these fields, then the endpoint is
called with some extra environment variables 'api.vendor', 'api.version', and
'api.format'. If the version couldn't be matched, then the middleware returns
404 and also sets the X-CASCADE header to pass. That last part is
important because it allows Rack::Mount
to keep looking for other endpoints which might match the version.
You can also control the routing behavior when no Accept header is specified
strict option. If
strict is set to true, then a 404 will be
returned when no Accept is set. If
strict is false, then the first matched
endpoint is returned. This is inline with the RFC definition and basically
means that the client doesn't care which version the server responds with.
Most likely, if
strict is set to false, you'd like to use the latest
available version. To achieve this, you should mount your latest version as
high as possible (similar to routes precedence in Rails)
class MyAPI < Grape::API # version v2 has higher precedence than v1 version :v2, :strict => false do get '/cows' do end end version :v1 do get '/cows' do end end end
Currently, path and header based versioning are what's understood, but we've opened up the possibility of custom versioning strategies. Prefer to do domain based versioning? Or IP-based versioning? If you want to get really wacky, you can even version based on the lunar calendar.
Extra thanks goes out to jwkoelewijn for creating the initial feature branch and kicking off the discussion.