Your API has paying clients. You need to change the response format for /users. Without versioning, every client breaks simultaneously. With versioning, existing clients continue working while new clients use the updated contract. The question is not whether to version your API, but which strategy to choose.
Rails 6 API Tutorial - Namespacing and versioning p.8
What are the three versioning strategies in Rails?
URL namespace versioning
The version lives in the URL: /api/v1/users, /api/v2/users. This is the most common approach because it is explicit and cacheable.
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users
end
namespace :v2 do
resources :users
end
end
end
Controllers are organized in matching directories:
# app/controllers/api/v1/users_controller.rb
module Api
module V1
class UsersController < ApplicationController
# v1 logic
end
end
end
Pros: Immediately visible in URLs, easy to debug, works with HTTP caching, simple to test.
Cons: Code duplication across versions (controllers, serializers, tests).
Header-based versioning
The URL stays clean (/api/users). The version is specified via HTTP headers like Accept: application/vnd.example.v1+json.
# lib/api_constraints.rb
class ApiConstraints
def initialize(options)
@version = options[:version]
@default = options[:default]
end
def matches?(req)
@default || req.headers['Accept'].include?("application/vnd.example.v#{@version}")
end
end
Pros: Clean URLs, less code duplication when versions share most logic.
Cons: Harder to test (requires simulating custom headers), not visible in browser, debugging is less intuitive.
Subdomain versioning
Each version gets its own subdomain: v1.api.example.com, v2.api.example.com.
# config/routes.rb
scope constraints: { subdomain: "v1.api" }, module: :api do
scope module: :v1 do
resources :posts
end
end
Pros: Complete isolation between versions, independent scaling, separate security policies.
Cons: Complex DNS configuration, wildcard SSL certificates needed, localhost does not support subdomains (requires /etc/hosts workarounds).
How do the strategies compare side by side?
| Strategy | Visibility | Code duplication | Caching | Setup complexity | Best for |
|---|---|---|---|---|---|
| URL namespace | High | Higher | Excellent | Low | Most Rails APIs |
| Header-based | Low | Lower | Requires config | Medium | APIs prioritizing clean URLs |
| Subdomain | High | Highest | Excellent | High | Architecturally distinct versions |
For most Rails projects, URL namespace versioning is the practical default. It aligns with Rails conventions and gives clients, developers, and monitoring tools clear visibility into which version is active.
How do you reduce code duplication across versions?
Rails concerns prevent repeating route definitions:
# config/routes.rb
Rails.application.routes.draw do
concern :api do
resources :users, :courses, :assignments
end
namespace :v1 do
concerns :api
end
namespace :v2 do
concerns :api
end
end
For controllers, extract shared logic into base classes or modules. Only version-specific differences live in the versioned controllers:
# app/controllers/api/base_users_controller.rb
module Api
class BaseUsersController < ApplicationController
private
def find_user
@user = User.find(params[:id])
end
end
end
# app/controllers/api/v2/users_controller.rb
module Api
module V2
class UsersController < Api::BaseUsersController
# Only v2-specific serialization and logic
end
end
end
Why does introducing versioning break existing clients?
“Ironically, introducing versioning itself can be a breaking change for production APIs!”
Clients calling /api/users suddenly get a 404 when you move the endpoint to /api/v1/users. The solution: route unversioned requests to a default version.
# config/routes.rb
Rails.application.routes.draw do
concern :api do
resources :users, :courses, :assignments
end
namespace :v1 do
concerns :api
end
namespace :v2 do
concerns :api
end
# Unversioned requests fall through to v1
scope module: 'v1' do
concerns :api
end
end
Now /users and /v1/users return identical responses. Clients migrate at their own pace.
How should you deprecate old API versions?
Use HTTP headers to signal deprecation programmatically:
# app/controllers/v1/users_controller.rb
class V1::UsersController < ApplicationController
before_action :add_deprecation_headers
private
def add_deprecation_headers
response.headers['Deprecation'] = 'true'
response.headers['Sunset'] = 'Wed, 31 Dec 2025 23:59:59 GMT'
response.headers['X-DEPRECATION-WARN'] = 'API v1 will be removed on 31 December 2025. Please migrate to v2.'
end
end
The Sunset header follows RFC 8594 and enables automated tools to detect upcoming deprecations.
Deprecation process:
- Announce deprecation schedule when the new version launches
- Add deprecation headers to all old-version responses
- Stop accepting new client registrations for the old version
- Restrict access to advanced features on the old version
- On the sunset date, return
410 Gonewith migration instructions (do not delete endpoints immediately) - Archive documentation for the deprecated version instead of removing it
Version support policy: The N-2 rule (support current version plus two previous) works well for most APIs. Industries with long approval cycles may need extended support windows.
“Deprecating an API doesn’t have to be a negative experience for your users. By being transparent, providing clear alternatives, and supporting your users through the transition, you can maintain trust and continue to build strong relationships with your developer community.”
— Treblle
What monitoring should you have on versioned APIs?
Track usage patterns across versions using Rails built-in analytics or tools like New Relic. Knowing which endpoints are heavily used on each version helps you:
- Prioritize migration support for high-traffic endpoints
- Identify clients that need proactive outreach before sunset dates
- Detect when an old version’s usage drops low enough to retire safely
Practical Implementation: The USEO Approach
API versioning decisions have consequences that last years. The initial choice is straightforward; the complexity emerges when real clients depend on your API and business requirements change.
On the Yousty HR portal (13-year partnership), the API started unversioned. When the first breaking change became necessary, we introduced URL namespace versioning with a backward-compatible routing layer. Existing integrations continued working on unversioned endpoints while new clients adopted /api/v1/. The transition took several months because enterprise HR systems have long update cycles. We maintained the unversioned routes for over a year before fully deprecating them.
The key technical decision was extracting shared controller logic into base classes early. When v2 was needed years later, only the serialization layer and new business rules required version-specific code. Controllers for v1 and v2 shared 70%+ of their implementation through inheritance.
For Triptrade (travel MVP), we took a different approach. As an MVP with no existing clients, we started with /api/v1/ from day one but kept the implementation simple. No base controller extraction, no shared concerns. The reasoning: premature abstraction wastes time when you do not yet know which parts of the API will change. When v2 became necessary after the first pivot, we refactored the shared logic at that point with clear knowledge of what actually differed between versions.
Versioning patterns we recommend based on project stage:
| Project stage | Recommended approach | Reasoning |
|---|---|---|
| MVP / new product | URL namespace, minimal abstraction | You do not know what will change yet |
| Growing product (2-5 clients) | URL namespace + shared base controllers | Duplication becomes costly |
| Mature product (10+ clients) | URL namespace + deprecation headers + monitoring | Client communication becomes critical |
| Enterprise integrations | URL namespace + extended N-3 support | Long approval cycles require patience |
Common mistakes we see in client projects:
- Versioning the entire API when only one endpoint needs a breaking change. Version specific resources, not the whole API.
- Forgetting to version serializers alongside controllers. The response format is part of the contract.
- No deprecation communication plan. Technical headers alone are insufficient. Email clients, update documentation, provide migration guides.
- Keeping deprecated versions running indefinitely. Set sunset dates and enforce them. Maintaining dead versions is a hidden cost.
FAQs
What factors determine the best versioning strategy for a Rails API?
Three factors matter most: how many clients consume the API (more clients favor explicit URL versioning), how frequently breaking changes occur (frequent changes favor header-based to reduce route proliferation), and your team’s operational capacity (subdomain versioning requires DNS and SSL management). For most Rails projects, URL namespace versioning provides the best balance of clarity and maintainability.
How do you phase out older API versions without breaking clients?
Announce deprecation at least 6 months before the sunset date. Add Deprecation and Sunset HTTP headers to all responses on the deprecated version. Communicate directly with high-value clients. Provide migration guides with endpoint mappings and code examples. On the sunset date, return 410 Gone rather than deleting endpoints, allowing emergency reactivation if critical clients are affected.
How do you manage multiple API versions without excessive code duplication?
Extract shared logic into base controllers and modules. Use Rails routing concerns to avoid repeating route definitions. Version only the parts that differ (typically serializers and version-specific business rules). Keep shared database queries, authorization logic, and common validations in inherited base classes.