Creating a RESTful, versioned API using Sinatra

I am relatively new to Ruby and recently built my first Sinatra application. The requirement was to create an API which would detail product information, arranged into a number of API feeds, in a number of data formats.

There are a lot of articles on the web about API versioning in a RESTful manor, so I shall not delve into the details regarding this, but I found this article by Peter Williams to be one of the most useful despite being a couple of years old.

For the purpose of this post, I will be working on the basis that the application would need to accept HTTP requests in the following format:

GET /retail/products HTTP/1.1
Accept: application/vnd.mycompany.myapp-v2+json

I decided to use the Sinatra framework to implement the API due to it’s simplicity. Sinatra would provide the basis for handling the HTTP requests, but the more interesting part of the implementation became apparent when deciding how to deal with the vendor mime time in the HTTP Accept header.

To implement the versioned API, three Rack middleware plugins were used:

  • Rack REST API Versioning
  • Rack Conneg
  • Rack Replace HTTP Accept

Rack REST API Versioning

The Rack REST API Versioning adapter does pretty much what it says on the tin; it is used parse the version number out of the Accept header and stores it in an environmental variable.

Rack Conneg

Rack Conneg (content negotiation) would be used to respond to a request in the requested format. Conneg parses the Accept header and provides a Rails-style respond_to block interface to allow the application to respond accordingly. Conneg uses the Rack::Mime class to map the Accept header to a data format, however when using a custom vendor mime type such as application/vnd.mycompany.myapp-v2+json, it would be necessary to teach the application how to map these to the desired data type.

Rack Replace HTTP Accept

I created the Rack Replace HTTP Accept adapter to cater for the above scenario. The adapter makes it possible to replace the Accept header based on a regular expressions, changing it into a format that Conneg can negotiate.

Below is a skeleton of the source code for the completed application. If anyone has any feedback I’d love to hear.

require 'rack/conneg'
require 'rack/replace_http_accept'
require 'rack/rest_api_versioning'
require 'sinatra'
# ...

class Application < Sinatra::Base
   # Set default API version
   use Rack::RestApiVersioning, :default_version => '1'

   # Create custom vendor mime type mappings
   use Rack::ReplaceHttpAccept, /application\/vnd\.mycompany\.myapp-v[0-9]+\+json/ => 'application/json',
   /application\/vnd\.mycompany\.myapp-v[0-9]+\+xml/  => 'application/xml'

   # Initalise Conneg
   use(Rack::Conneg) { |conneg|
      conneg.set :accept_all_extensions, false
      conneg.provide([:json, :xml])

   get '/retail/products' do
      version = env['api_version']
      respond get_product_data(version)

   def get_product_data version
      # ...

   def respond data
      respond_to do |wants|
         wants.json  {
            # Set Content-Type header accordingly
            content_type "application/vnd.mycompany.myapp-v#{version}+json"
         wants.xml   {
            content_type "application/vnd.mycompany.myapp-v#{version}+xml"
         wants.other {
            content_type 'text/plain'
            error 406, "Not Acceptable"

   before do
      if negotiated?
         # Important: resource cacheable based on Content-Type
         headers "Vary" => "Content-Type"

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s