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?

StrategyVisibilityCode duplicationCachingSetup complexityBest for
URL namespaceHighHigherExcellentLowMost Rails APIs
Header-basedLowLowerRequires configMediumAPIs prioritizing clean URLs
SubdomainHighHighestExcellentHighArchitecturally 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:

  1. Announce deprecation schedule when the new version launches
  2. Add deprecation headers to all old-version responses
  3. Stop accepting new client registrations for the old version
  4. Restrict access to advanced features on the old version
  5. On the sunset date, return 410 Gone with migration instructions (do not delete endpoints immediately)
  6. 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 stageRecommended approachReasoning
MVP / new productURL namespace, minimal abstractionYou do not know what will change yet
Growing product (2-5 clients)URL namespace + shared base controllersDuplication becomes costly
Mature product (10+ clients)URL namespace + deprecation headers + monitoringClient communication becomes critical
Enterprise integrationsURL namespace + extended N-3 supportLong approval cycles require patience

Common mistakes we see in client projects:

  1. Versioning the entire API when only one endpoint needs a breaking change. Version specific resources, not the whole API.
  2. Forgetting to version serializers alongside controllers. The response format is part of the contract.
  3. No deprecation communication plan. Technical headers alone are insufficient. Email clients, update documentation, provide migration guides.
  4. 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.