Versioning APIs is a critical part of building web applications, as it allows you to make changes that may otherwise break existing API users. Changing the contract between the API and the clients that depend on it is dangerous, and versioning endpoints adds flexibility and safety. Versioning is implemented in many ways - You can version with subdomains, query parameters, URL schemas, headers, and more!
Still, developers looking to introduce a new version of an API that wasn't originally versioned may find themselves in a pickle. Clients may be calling an endpoint that takes the form base_url/all_users
, so replacing it with something like base_url/v1/users
presents an obvious problem. In this article, we'll explore how to version an API built with Ruby on Rails and walk through options for versioning an existing Rails API.
Why versioning is important
Change is one of the only guarantees in programming. APIs often change over time to fit business needs, fix bugs, introduce improvements, or simply change functionality. Many changes can be made without affecting the contract between the API and the client, but some changes unavoidably introduce something the caller of the API does not expect.
API updates that change behavior in a way that may surprise existing clients of the API are called "breaking changes." If you're renaming an endpoint, callers of the API using the old endpoint name will immediately experience HTTP 404 errors when your update is deployed. Introducing new required parameters, changing HTTP verbs, or changing the API response can all introduce breaking changes.
If an API is versioned, breaking changes can be rolled out to a new version of the API, and clients can call the new version when they've had a chance to update their code to adapt to the breaking changes. An endpoint previously named base_url/v1/all_users
can be renamed in a v2
version, and clients can call the new base_url/v2/users
endpoint without interrupting the application's health. Still, versioning an API comes at the cost of increased maintenance of multiple sets of endpoints with similar logic. You can read more about API versioning in this great resource from Postman.
Namespacing in Rails routes
Fortunately for Rails developers, versioning is an established problem with an established solution! While there are many ways to version an API, it is common to use a version number directly in the route of an endpoint. As discussed above, a versioned route would look something like base_url/v1/notes
and base_url/v2/notes
. Sometimes, the URL is the same outside of the version, but it can differ!
Namespacing routes
To introduce versioning into an API, you will first need to add namespaces for your versions in the routes file of your rails app. You can do this to an existing API! Let's take a look at an example config/routes.rb
:
root to: 'home#index'
resource :users
resource :courses
resource :assignments
If you wanted to introduce a single v1
to these resources, you should wrap them in a namespace:
root to: 'home#index'
namespace :v1 do
resource :users
resource :courses
resource :assignments
end
This is useful if you know that you only have one version for now, but adding a second version is simple. You can introduce a set of v2
endpoints just as easily:
root to: 'home#index'
namespace :v1 do
resource :users
resource :courses
resource :assignments
end
namespace :v2 do
resource :users
resource :courses
resource :assignments
end
This introduces some duplication to the routes file, which is a code smell that we can easily remedy. Introducing concerns into the routes file can fix this. The above config/routes.rb
can be changed to the following with the same result:
root to: 'home#index'
concern :api do
resource :users
resource :courses
resource :assignments
end
namespace :v1 do
concerns :api
end
namespace :v2 do
concerns :api
end
Versioning Rails controllers
These namespaced routes will direct traffic to the appropriate namespaced controllers, so you'll need to change your controller names to match. To begin, create the appropriate directories under app/controllers
. If you've introduced a v1
namespace, then create a directory, app/controllers/v1/
. Next, move any controllers that will handle traffic for your v1
namespace under this new directory.
Lastly, you'll need to rename the controllers themselves to match the namespace. If the Courses controller class is defined as the following:
class CoursesController < ApplicationController
Then, to move it to the v1 namespace, redefine it as
class V1::CoursesController < ApplicationController
Repeat this process for any controllers in the v1
namespace, and your application will now have fully functional versioned endpoints!
Handling unversioned requests for backwards compatibility
Introducing versions to a production API that doesn't already have them is potentially a breaking change itself, which is ironic! Existing clients of the API would be calling routes that no longer exist until they are updated to provide a version in the URL.
Fortunately, we can remedy this inside our Rails API. We can provide a default route so unversioned requests are routed to a namespace of our choice, like v1
.
You can do this in routes.rb
with a scope
block that references your api
concern. The Rails Guides explain this well in the routing section. Add the scope block to the bottom of your config/routes.rb
file like this:
root to: 'home#index'
concern :api do
resource :users
resource :courses
resource :assignments
end
namespace :v1 do
concerns :api
end
namespace :v2 do
concerns :api
end
scope module: 'v1' do
concerns :api_base
end
Conclusion
From leveraging namespaces in routing to organizing controllers within version-specific directories, these essential components of an API allow for ease of change. They provide a structured approach to evolve APIs while safeguarding against potential breaking changes. Whether you're a seasoned developer or new to the Rails ecosystem, the insights shared here equip you with the knowledge to manage API versions effectively, ensuring your applications continue to support clients reliably even through major changes.