api-versioning
API versioning strategies including URL-based, header-based, and breaking change management
When & Why to Use This Skill
This Claude skill provides comprehensive strategies and implementation patterns for API versioning, including URI and header-based approaches. It empowers developers to manage breaking changes effectively, design backward-compatible systems, and implement graceful deprecation workflows, ensuring a seamless and stable experience for API consumers during evolution.
Use Cases
- Managing breaking changes: Safely introducing major updates to API contracts without disrupting existing client integrations.
- Multi-version support: Implementing and routing multiple concurrent API versions using URI paths or custom request headers.
- Deprecation workflows: Designing and executing clear API retirement strategies with automated warnings and grace periods.
- Backward compatibility: Utilizing adapters and schema evolution techniques to maintain support for legacy fields and structures.
- Gateway integration: Configuring API gateways to handle version-based routing and provide compatibility metadata to clients.
| name | api-versioning |
|---|---|
| description | API versioning strategies including URL-based, header-based, and breaking change management |
API Versioning Skill
Use this skill when:
- Designing API versioning strategies
- Managing breaking changes
- Creating deprecation workflows
- Implementing API gateways
- Designing backward-compatible APIs
- Planning multi-version support
When to Use
Use this skill when:
- Your API needs to evolve without breaking existing clients
- You're planning breaking changes to API contracts
- Designing multi-version support
- Implementing API deprecation strategies
- Creating API gateway patterns
Key Scenarios
- Major API Changes: Breaking changes to existing contracts
- Feature Additions: New endpoints or parameters
- Field Modifications: Changing field types or structures
- Deprecation: Removing deprecated endpoints
- Gateway Integration: Proxying multiple versions
Versioning Strategies
1. URI Path Versioning
# config/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :put_secure_browser_headers
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_user_token
end
# v1 APIs
scope "/api/v1", as: :api do
pipe_through :api do
get "/users", UserController, :index
post "/users", UserController, :create
get "/users/:id", UserController, :show
put "/users/:id", UserController, :update
delete "/users/:id", UserController, :delete
end
# v2 APIs (breaking changes)
scope "/api/v2", as: :api do
pipe_through :api do
get "/users", UserV2Controller, :index
post "/users", UserV2Controller, :create
get "/users/:id", UserV2Controller, :show
put "/users/:id", UserV2Controller, :update
delete "/users/:id", UserV2Controller, :delete)
end
end
end
2. Header-Based Versioning
# config/config.exs
config :my_app, MyAppWeb.Gateway,
api_version: "v2"
defmodule MyAppWeb.Gateway do
def call(conn, opts) do
# Accept version from header
case get_header(conn, "api-version", nil) do
nil -> {:ok, conn}
version when is_binary(version) ->
Logger.info("API version from header: #{version}")
{:ok, put_private(conn, :api_version, version)}
end
end
end
# In controller
defmodule UserV2Controller do
alias MyAppWeb.Gateway
def index(conn, params) do
version = get_api_version(conn)
case version do
nil ->
# No version specified, use latest
list_users_v2(conn, params)
"v1" ->
# Forward to v1 controller for backward compatibility
UserV1Controller.index(conn, params)
"v2" ->
list_users_v2(conn, params)
_ ->
conn
|> put_status(400)
|> json(%{error: "Unsupported API version: #{version}"})
end
end
defp get_api_version(conn) do
Map.get(conn.private, :api_version, "v2")
end
end
Backward Compatibility
1. Deprecation Warnings
defmodule MyApp.Deprecation do
use GenServer
require Logger
# Client API
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
def deprecate(feature_name, deadline_date), do: GenServer.cast(__MODULE__, {:deprecate, feature_name, deadline_date})
def is_deprecated?(feature_name), do: GenServer.call(__MODULE__, {:is_deprecated, feature_name})
def get_deprecation_info(feature_name), do: GenServer.call(__MODULE__, :get_deprecation_info, feature_name})
@impl true
def init(opts), do
Logger.info("Starting deprecation service")
{:ok, %{
deprecated_features: %{},
opts: opts
}}
end
@impl true
def handle_cast({:deprecate, feature_name, deadline_date}, state) do
new_deprecated = %{
feature_name: %{
deprecated_at: DateTime.utc_now(),
deadline_date: deadline_date
}
}
new_state = put_in(state, :deprecated_features, feature_name, new_deprecated)
{:noreply, new_state}
end
@impl true
def handle_call({:is_deprecated, feature_name}, _from, state) do
case Map.get(state.deprecated_features, feature_name) do
nil ->
{:reply, false, state}
%{deprecated_at: deadline_date} ->
now = DateTime.utc_now()
if DateTime.compare(now, deadline_date) == :gt do
Logger.error("Feature #{feature_name} is deprecated and past deadline")
{:reply, true, state}
else
{:reply, false, state}
end
_deprecated_info ->
{:reply, true, state}
end
end
def handle_call({:get_deprecation_info, feature_name}, _from, state) do
case Map.get(state.deprecated_features, feature_name) do
nil ->
{:reply, nil, state}
info -> {:reply, info, state}
end
end
end
2. Field Evolution
# Schema evolution with Ash
defmodule MyApp.Accounts.User do
use Ash.Resource
attributes do
uuid_primary_key :id
attribute :email, :string, allow_nil?: false
attribute :name, :string
# Deprecated field - marked for removal
attribute :old_field, :string, deprecated_reason: "Replaced by new_field", allow_nil?: true
attribute :new_field, :string
timestamps()
end
actions do
read :read
update :update
create :create
destroy :destroy
end
end
# Migration for deprecation
defmodule MyApp.Repo.Migrations.DeprecateOldField do
use Ecto.Migration
def change do
alter table(:users) do
add :new_field, :string
end
# Add deprecation warning to queries
create index(:users, [:email, :new_field])
end
def down do
drop index(:users, [:email, :new_field])
alter table(:users) do
remove :new_field
end
end
end
3. Adapters for Backward Compatibility
defmodule MyApp.Api.V1ToV2Adapter do
require Logger
def adapt_request(conn, params) do
Logger.info("Adapting v1 request to v2 structure")
# Map v1 params to v2 structure
v2_params = %{
"user_id" => params["id"],
"email" => params["email"],
"password" => params["password"],
# New required fields
"full_name" => params["full_name"] || params["name"],
"phone" => params["phone"]
}
Logger.debug("Mapped v1 params to v2: #{inspect(v2_params)}")
{:ok, v2_params}
end
def adapt_response(v2_response) do
Logger.info("Adapting v2 response to v1 structure")
# Map v2 response to v1 structure
v1_response = %{
"id" => v2_response.id,
"email" => v2_response.email,
"name" => v2_response.full_name || v2_response.name,
"phone" => v2_response.phone
"created_at" => v2_response.inserted_at
}
Logger.debug("Mapped v2 response to v1: #{inspect(v1_response)}")
{:ok, v1_response}
end
end
Breaking Change Management
1. Semanic Versioning
defmodule MyApp.Versioning do
@major "1.0"
@minor "0"
@patch "0"
@pre_release "dev"
def current_version do
@major <> "." <> @minor <> "." <> @patch
end
def next_major_version, do
next_major = String.to_integer(@major) + 1)
%{current_version | major: Integer.to_string(next_major), minor: @minor, patch: @patch}
end
def next_minor_version, do
next_minor = String.to_integer(@minor) + 1)
%{current_version | major: @major, minor: Integer.to_string(next_minor), patch: @patch}
end
def next_patch_version, do
next_patch = String.to_integer(@patch) + 1)
%{current_version | major: @major, minor: @minor, patch: Integer.to_string(next_patch)}
end
def bump_version(version_type) do
case version_type do
:major -> next_major_version()
:minor -> next_minor_version()
:patch -> next_patch_version()
:pre -> current_version()
end
end
def is_backward_compatible?(v1, v2) do
# Parse versions
v1_parts = String.split(v1, ".")
v2_parts = String.split(v2, ".")
{v1_major, v1_minor, _v1_patch} = v1_parts
{v2_major, v2_minor, _v2_patch} = v2_parts
# v1 < v2: backward compatible
cond do
String.to_integer(v1_major) < String.to_integer(v2_major) -> true
String.to_integer(v1_major) == String.to_integer(v2_major) ->
String.to_integer(v1_minor) < String.to_integer(v2_minor) -> true
true -> false
end
end
end
end
2. Breaking Change Policy
defmodule MyApp.Api.BreakingChangePolicy do
require Logger
# Breaking change levels
@levels [:minor, :major, :breaking]
@policy %{
:minor => %{
deprecation_period: 90, # 90 days
warnings: true,
client_notification: true,
grace_period: 7 # 7 days
},
:major => %{
deprecation_period: 180, # 180 days
warnings: true,
client_notification: true,
grace_period: 30 # 30 days
grace_period: 7 # 7 days
},
:breaking => %{
deprecation_period: 0,
warnings: true,
client_notification: true,
grace_period: 0
client_notification: true,
requires_major_version_bump: true
}
}
# Client API
def check_compatibility(version, feature), do
# Get feature version from config
feature_versions = Application.get_env(:my_app, :feature_versions, %{})
case Map.get(feature_versions, feature) do
nil -> {:ok, :compatible}
feature_version -> is_backward_compatible?(version, feature_version)
end
end
def get_deprecation_deadline(feature, change_level) do
policy = Map.get(@policy, change_level, @policy[:minor])
days = policy[:deprecation_period]
Date.add(Date.utc_today(), days)
end
# Notify clients
def notify_deprecation(feature, deadline, policy, change_level) do
if policy[:client_notification] do
Logger.warning("Deprecating #{feature}, deadline: #{deadline}")
# Send notification to clients
MyApp.Notifier.notify_clients(:feature_deprecation, %{
feature: feature,
deadline: deadline,
change_level: change_level
})
end
end
end
API Gateway Patterns
1. Version-Based Routing
defmodule MyApp.Gateway.Router do
use Plug.Router
@api_versions ["v1", "v2", "v3"]
plug Plug.Static, at: "/", from: :v1
plug Plug.Static, at: "/v1", from: :v2
plug Plug.Static, at: "/v2", from: :v3
# Default version (latest)
@api_version "v3"
# Health check endpoint
get "/health", HealthCheckController, :check
# API version endpoints (with latest alias)
scope "/", as: :api do
pipe_through :api do
# Latest version routes (aliased to /v3)
get "/users", UserV3Controller, :index
post "/users", UserV3Controller, :create
end
end
# Versioned endpoints
Enum.each(@api_versions, fn version ->
scope "/#{version}", as: :api do
pipe_through :api do
get "/users", :"UserController#{version}", :index
post "/users", :"UserController#{version}", :create
end
end)
end
end
2. Version Compatibility Headers
defmodule MyApp.VersionHeaders do
@supported_versions ["v1", "v1.1", "v2", "v2.1", "v3"]
def validate_version(conn) do
case get_header(conn, "api-version", nil) do
nil ->
{:ok, nil}
version ->
if version in @supported_versions do
{:ok, version}
else
{:error, :unsupported_version}
end
end
end
def add_version_headers(conn) do
supported_versions = Enum.join(@supported_versions, ", ")
conn
|> put_resp_header("api-version", @api_version)
|> put_resp_header("api-supported-versions", supported_versions)
|> put_resp_header("api-latest-version", @api_version)
end
end
Best Practices
DO
✅ Choose versioning strategy upfront ✅ Document version semantics clearly ✅ Provide deprecation warnings ✅ Implement graceful degradation ✅ Use semantic versioning ✅ Test backward compatibility ✅ Document breaking changes in CHANGELOG ✅ Notify clients of deprecations ✅ Support multiple versions when needed ✅ Consider API gateways for evolution
DON'T
❌ Change API versioning strategy mid-project ❌ Forget to document breaking changes ❌ Remove v1 immediately without deprecation ❌ Skip deprecation warnings ❌ Make breaking changes without grace period ❌ Ignore backward compatibility ❌ Forget to version databases ❌ Use ambiguous version numbers ❌ Skip client notification ❌ Make breaking changes without documentation
Integration with ai-rules
Roles to Reference
- Architect: Use for API design and versioning strategy selection
- Orchestrator: Implement versioning in features
- Backend Specialist: Design version-aware APIs
- Reviewer: Verify version compatibility and deprecation policies
- QA: Test backward compatibility
Skills to Reference
- api-design: Use versioning in API design
- test-generation: Write tests for version compatibility
- observability: Monitor deprecated API usage
- distributed-systems: Combine with distributed API patterns
Summary
API versioning provides:
- ✅ URI path versioning
- ✅ Header-based versioning
- ✅ Breaking change management
- ✅ Deprecation workflows
- ✅ Backward compatibility patterns
- ✅ Multi-version support
Key: Choose strategy upfront, document changes, deprecate gracefully, and maintain backward compatibility.