API versioning helps alter an API’s behavior for different clients. An API version is determined by an incoming client request and may either be based on the request URL or on the request headers. There are a number of valid approaches to versioning.
When is API versioning needed?
API versioning can be ignored in certain cases, such as when an API serves as an internal client or when an API you’ve already been using undergoes some minor changes (such as adding new fields or new data to the response).
But if you make some critical changes to your code or the business logic of your app and these changes could affect existing clients, then API versioning is the only way to avoid breaking old clients.
Read also: How to Speed Up Your Ruby on Rails Software Product
How can an API version be specified by the client?
Here’s a list of places where API versions are commonly specified:
1. URL Path parameter:
The API version is placed inside the URL path
HTTP GET:
https://domain.com/api/v2/resources
2. URL Get Parameter or request body parameter
HTTP GET:
https://domain.com/api/resources?version=v2
3. Accept Header as versioned media type
HTTP GET:
https://domain/api/books
Accept:
application/vnd.your_app_name.v2+json
4. Custom Header
HTTP GET:
https://domain/api/books
API-VERSION: 2
There’s an ongoing debate as to the proper way of specifying an API version.
URLs aren’t considered ideal for this task since they represent a resource but not the version of that resource. Still, this is the easiest approach and is useful for testing.
A custom header is considered excessive since the HTTP specification already has the Accept header that serves the same purpose.
Accept Header API versioning looks the best option according to the HTTP specification. But it’s not easy to test such APIs compared with other approaches. Since it’s not enough to open an API URL, you have to compose a request with proper headers.
When it comes to which version of an API it’s better to choose, most developers agree that using the very first API version by default is the best choice. If your API client (iOS/Android device, web browser, etc.) doesn’t specify a required API version, your API will have to return the very first version of the response since the only safe assumption is that this client was made before the API had any versioning at all.
API versioning with Ruby on Rails
Rails has a huge set of gems for building APIs with versioning. Let’s look at their capabilities in detail.
-
versionist
This gem supports three versioning strategies: HTTP header, URL path, and request parameter. Routes, controllers, presenters/serializers, tests, and documentation are namespaced. This isolates the code of one API version from another. This might seem excessive, since most changes are made inside views or serializers. But it’s more correct, since isolating logic inside namespaces is a cleaner and more obvious approach than dealing with a mix of different versions inside one controller.
To automate routine tasks, versionist provides Rails generators for generating new versions of your API as well as new components within an existing version. It also provides a Rails generator that copies an existing API version to a new API version. This doesn’t work according to the DRY approach, however, since it results in code duplication. I’ve never used these generators. I usually create all needed controllers and serializers manually. I also don’t copy all code from the previous version; I just inherit from the previous version controller.
One significant downside of the versionist gem is that the API versioning mechanism it provides doesn’t support fallbacks to the previous version if the specified logic hasn’t been copied to the new version. The gem expects all required code to be duplicated in each new version. But if you have to change just one response format, this seems excessive.This gem is still quite good, though. It’s lightweight and is centered around API versioning only. That’s nice in comparison with some gems that actually dictate specific methods of API versioning (rocket_pants and versioncake, for instance).
Here’s an example of versioned routes from the versionist gem using the Accept Header with the versioned media type:
namespace :versionist_api do
api_version(
header: {
name: "Accept",
value: 'application/vnd.versionist_api.v2+json'
},
module: "V2",
defaults: { format: :json }
) do
resources :books, only: [:index, :create, :show, :update, :destroy]
end
api_version(
header: {
name: 'Accept',
value: 'application/vnd.versionist_api.v1+json'
},
module: 'V1',
default: true,
defaults: { format: :json }
) do
resources :books, only: [:index, :create, :show, :update, :destroy]
end
end
-
versioncake
This gem takes a different approach. Mostly, versioning happens for API views, and controllers aren’t namespaced. One nice feature of versioncake is that it has fallbacks to previous versions. Along with path, query param, accept header, and custom header it also provides the ability to create your own versioning approach that accepts a request object. This lets developers specify an API version at any place in the request in any form they need.
Since versioncake doesn’t support a controller for each version, it has special methods to access the requested version and the latest version inside the controller’s instance. However, this might lead an inexperienced developer to write bad code if they have some conditional logic inside controllers that depends on these version parameters. In this case, it’s better to use the factory pattern where controller action is implemented as a single-purpose object for each version (the interactor gem can be used for this purpose).
Versioncake has a rich set of features (for details, look at the comparison table), including some exotic features like version deprecation. In one way, it looks like a complete solution for API versioning; but in another, it seems a little bit heavy since some of its extra features might not be used at all in generic API use cases.
Also, one downside of versioncake is that it’s view oriented. Gems like jbuilder and rabl can be used with versioncake since their templates are saved as views. But more modern and popular gems like active_model_serializers can’t be used with versioncake. This might be okay if you prefer reusing some view parts as partials (for example, when fields from version 1 are present in a version 2 response); with active_model_serializers you can use normal Ruby class inheritance.
-
grape
Grape isn’t just an API versioning tool; it’s a REST-like API framework. Grape is designed to run on Rack or complement existing web application frameworks such as Rails and Sinatra by providing a simple Domain Specific Language to easily develop RESTful APIs.
As for API versioning, grape offers four strategies: URL Path, Accept Header (similar to the versioned media type approach), Accept-Version Header, and Request Parameter.
It’s also possible to have fallbacks to previous versions using the specific code organization described here. Here’s a short example of API versioning fallbacks in grape:
And here’s a module for default configuration of the first version:
module GrapeApi
module V1
module Defaults
extend ActiveSupport::Concern
included do
# this would let first api version respond to second as fallback
version ['v2', 'v1'], using: :header, vendor: 'grape_api'
# ….
end
end
end
And the second version:
module GrapeApi
module V2
module Defaults
extend ActiveSupport::Concern
included do
# version "v2", using: :path
version 'v2', using: :header, vendor: 'grape_api'
end
end
end
At grape_api/base.rb, the second version is mounted before the first. This lets requests to version 2 be processed with V2 logic (if it exists) or to fall back to version 1.
module GrapeApi
class Base < Grape::API
mount GrapeApi::V2::Base
mount GrapeApi::V1::Base
end
End
To me, the API version callbacks described above look heavy and complex. You have to copy almost all basic configuration files and code for each version, which isn’t a DRY approach.
Probably there’s more sense in using grape as a Rack application than using it in conjunction with Rails/Sinatra since you have to duplicate the Rails structure to use it properly. And if you have Rails, then you don’t need to add grape. Plus, since this framework is not as popular as Rails, you might have create your own integration with some external services like NewRelic. For more details, read this articleabout pros and cons of existing Rails API solutions.
From my experience, each new API server project has some admin panel that’s usually made with the active_admin gem. That’s why it doesn’t make sense to use the grape gem in this case. The same goes for using an API-only Rails configuration. If a project needs an admin panel on the same server, then we can’t use this API-only configuration.
-
rocket_pants
This gem isn’t just for API versioning. It has many more features for building APIs.
For the moment, rocket_pants isn’t compatible with the newest Rails 5, however. Maybe this will change in future. Because of this limitation, I wasn’t able to try it manually.As for versioning strategies, rocket_pants has Path parameter support only. It has namespaced routes and controllers. As a part of default configuration for view versioning, it uses serializable_hash method; it uses serializable_hash method of the Model layer, meaning that view versioning isn’t possible. But according to the gem’s documentation, it has support for the active_model_serializers gem. In your expose call that’s used for data rendering, it’s possible to pass a serializer with :serializer or :each_serializer options. This lets you specify a namespaced serializer for a particular API version.
With the rocket_pants gem, API versioning in routes is defined as follows:
api version: 2 do
get 'x', to: 'test_a#item'
end
api versions: 1..3 do
get 'y', to: 'test_b#item'
end
I assume that this might work as a fallback for `y` in the route request for version 2.
Since this gem is outdated, I think that it’s better to avoid it, but you might decide you want to create a custom solution similar to it, that’s why we mentioned it here.
-
api-versions
This gem is also outdated (at the time of writing the last commit was made on 23 February 2015), but it’s still compatible with Rails 5 so I was able to play with it.
This gem supports only one versioning strategy: Accept Header Version (versioned media type). Some people in the Rails community consider this versioning approach to be the most correct. Read here for more details.
I also prefer this versioning approach.
Versioned routes with this gem look like this:
api vendor_string: "api_versions",
default_version: 1,
path: 'api_versions', module: 'api_versions' do
version 1 do
cache as: 'v1' do
resources :books, only: [:index, :create, :show, :update, :destroy]
controller :test, path: 'test' do
get 'some_action'
end
end
end
version 2 do
inherit from: 'v1'
end
end
The api-versions gem lets you inherit route definitions from a previous version. But this requires you to have a controller with a specified action for this version as well. The gem has a generator task (api_versions:bump) that creates controllers for the next version. Controllers that have been generated by this generator controllers inherit from previous ones. Inheritance by default seems to be the right solution in comparison with the versionist gem, which creates copies of previous controller versions.
But needing to create all controllers for each new API version doesn’t seem so great if you make just one small change to some API response. The better and more logical solution would be if this gem could provide an API version response fallback mechanism that would directly reuse the logic from the previous API version, but unfortunately api-versions doesn’t have such abilities.
-
acts_as_api
This gem isn’t made for API versioning, but rather provides a simple Domain Specific Langiage to define the representation of model data that should be rendered in API responses. It’s still possible to use it for API versioning, however.
Here’s how I experimented with acts_as_api to obtain versioning of API responses:
class Book < ApplicationRecord
belongs_to :author
accepts_nested_attributes_for :author
acts_as_api
api_accessible :base_fields do |t|
t.add :id
t.add :title
t.add :description
end
api_accessible :v1, extend: :base_fields do |t|
t.add lambda { |book|
"#{book.author.first_name} #{book.author.last_name}"
}, as: :author_name
end
api_accessible :v2, extend: :base_fields do |t|
t.add :updated_at
t.add :author
end
end
I don’t recommend using this gem, however, since it severely violates the MVC principle by placing the API response definition at the model level. Sure, you could extract these definitions into concerns/modules and include them in models. But still, it’s a very strange approach.
Rails API versioning tools comparison:
Strictly speaking, this approach isn’t Rails specific. In short, it’s a versioning system built around a series of gates. A gate is similar to a flag used to determine what functionality is allowed. Gates are connected to dates when a specific API change has been made. The backend keeps the date of the first client’s request. From this time on, each client request is filtered through these gates, which have conditional logic that defines allowed request parameters and response formats. This set of gates defines a user’s API version for a particular resource.
Read also: Advanced Video Processing for Ruby on Rails with FFmpeg Filters and Frei0r Plugin Effects
Version gate approach
Sure, this is a very complex solution. In its simplest form, this might create a mess of many conditional statements in controllers and views. If I had to implement such a solution, I would probably create a single-purpose object for each gate. Then these gates should be registered in a request processing chain and be processed one by one. This should help with avoiding a mess of if statements.
This approach is described in more details in this article.
Custom solution for fallbacks
If you aren’t satisfied with existing API versioning solutions, you can create your own. Here’s a description of an API versioning approach that’s inspired by the route constraint from this API Versioning Railscast and the API fallbacks solution from this article.The main part is this routes constraint class:
module CustomApi
class Constraints
def initialize(options)
@version = options[:version]
@default = options[:default]
end
def matches?(req)
@default || req.headers['Accept'] =~ regex
end
private
def format_version
if @version.instance_of?(Array)
@version.join(',')
else
@version
end
end
def regex
/application\/vnd.custom_api.v[#{format_version}]\+json/
end
end
end
CustomApi::Constraints uses regex to deal with multiple matching API versions:
In routes.rb the API is used like this:
namespace :custom_api, defaults: { format: 'json' } do
scope module: :v2, constraints: CustomApi::Constraints.new(version: 2) do
resources :books, only: [:index, :create, :show, :update, :destroy]
end
scope module: :v1, constraints: CustomApi::Constraints.new(
version: [1, 2],
default: :true
) do
resources :books, only: [:index, :create, :show, :update, :destroy]
controller :test, path: 'test' do
get 'some_action'
end
end
end
Order matters here, and V2 routes should be defined before V1. This lets the newest V2 responses be matched before V1 responses. If some route hasn’t been matched at the V2 scope, then it can be caught by the V1 module since the second version is also specified (version: [1, 2]). This simple solution deals with API response fallbacks without duplicating code from previous versions.
This lets `test/some_action` be processed for API versions 1 and 2.
I also have controllers with versioned namespaces:
app/controllers/custom_api/
├── v1
│ ├── base_controller.rb
│ ├── books_controller.rb
│ └── test_controller.rb
└── v2
├── base_controller.rb
└── books_controller.rb
As you can see, I don’t duplicate test_controller.rb in version 2.
The base controller inherits from the previous controller in V2:
module ApiVersions
module V2
class BaseController < ::ApiVersions::V1::BaseController
end
end
end
The same goes for the books controller:
module ApiVersions
module V2
class BooksController < ::ApiVersions::V1::BooksController
private
def book_params
params.require(:data).permit(
:title, :description, author_attributes: [
:first_name, :last_name, :biography
]
)
end
end
end
end
I only redefined the necessary methods.
Also, I use versioned serializers from the active_model_serializers gem. They inherit from previous API versions as needed.
Previously, I had been using the versionist gem. Probably on my next project, however, I’ll try this custom approach.
Read also: How to Speed Up Your Ruby on Rails App
Wrapping up
There’s a huge set of gems and approaches for API versioning with Rails. Some are nice, some are weird. You can use whatever solution fits your needs best. I would prefer using the simple custom solution I’ve described above.
Ten articles before and after
When and Why Angular Is a Good Technical Solution for Your Project
How to Develop a Multi-Language Ruby on Rails Application and Take It Global
Testing Web Software: Unit Testing Methods
Real-Time Features: Best Use Cases and Reason To Implement Them In Your App
Golang and Node.js Comparison: Scalability, Performance, and Tools
What You Can Create with FFmpeg Filters and Frei0r Plugin Effects
How to Use the Android NDK When Writing an App in Kotlin
Integrating SiriKit in a Third-Party iOS App
Using Machine Learning Algorithm for Predicting House Valuations
Introducing Horizon, Our Open Source Library for Sound Visualization