API Versioning Guide
Armature provides comprehensive API versioning support with multiple strategies for version extraction and flexible version management.
Table of Contents
- Overview
- Features
- Versioning Strategies
- Basic Usage
- Version Configuration
- Versioned Handlers
- Request and Response Extensions
- Migration Strategies
- Best Practices
- API Reference
- Summary
Overview
API versioning allows you to evolve your API over time while maintaining backward compatibility. Armature supports multiple versioning strategies:
- URL Path:
/v1/users,/v2/users - Header:
X-API-Version: 1 - Query Parameter:
/users?api-version=1 - Media Type:
Accept: application/vnd.myapi.v1+json
Features
- โ Multiple versioning strategies
- โ Semantic versioning support (1.0, 2.1, etc.)
- โ Version constraints (exact, minimum, range)
- โ Deprecated version warnings
- โ Response header injection
- โ Combined/fallback strategies
- โ Version-specific route handlers
Versioning Strategies
URL Path Versioning
The most common and visible approach:
use armature_core::versioning::{VersioningStrategy, ApiVersion};
let strategy = VersioningStrategy::url_path();
// Matches: /v1/users, /v2/users/123, /v3/posts
With a prefix:
let strategy = VersioningStrategy::url_path_with_prefix("/api");
// Matches: /api/v1/users, /api/v2/users
Header Versioning
Version in HTTP header (cleaner URLs):
let strategy = VersioningStrategy::header();
// Uses X-API-Version header by default
// Or custom header name
let strategy = VersioningStrategy::header_with_name("API-Version");
Client sends:
GET /users HTTP/1.1
X-API-Version: 2
Query Parameter Versioning
Version in query string:
let strategy = VersioningStrategy::query_param();
// Uses api-version parameter by default
// Or custom parameter name
let strategy = VersioningStrategy::query_param_with_name("version");
URL: /users?api-version=2
Media Type Versioning
Version in Accept header (RESTful approach):
let strategy = VersioningStrategy::media_type("vnd.myapi");
Client sends:
GET /users HTTP/1.1
Accept: application/vnd.myapi.v2+json
Combined Strategies
Try multiple strategies in order:
// Default: URL path โ Header โ Query param
let strategy = VersioningStrategy::default_combined();
// Custom order
let strategy = VersioningStrategy::combined(vec![
VersioningStrategy::header(),
VersioningStrategy::query_param(),
VersioningStrategy::url_path(),
]);
Basic Usage
Extracting Version from Request
use armature_core::{HttpRequest, HttpResponse, Error};
use armature_core::versioning::{VersioningStrategy, VersionedRequest, ApiVersion};
async fn handle_request(req: HttpRequest) -> Result<HttpResponse, Error> {
let strategy = VersioningStrategy::url_path();
// Extract version
let version = req.api_version(&strategy)
.unwrap_or(ApiVersion::V1);
// Route based on version
match version.major {
1 => handle_v1(req).await,
2 => handle_v2(req).await,
_ => Err(Error::BadRequest(format!(
"Unsupported API version: {}",
version
))),
}
}
Convenience Methods
use armature_core::versioning::VersionedRequest;
// URL path version
let version = req.url_version();
// Header version
let version = req.header_version("X-API-Version");
// Query param version
let version = req.query_version("api-version");
Adding Version to Response
use armature_core::versioning::{VersionedResponse, ApiVersion};
let response = HttpResponse::ok()
.with_json(&data)?
.with_api_version(ApiVersion::V2);
Version Configuration
Basic Configuration
use armature_core::versioning::{VersionConfig, VersioningStrategy, ApiVersion};
let config = VersionConfig::new(VersioningStrategy::url_path())
.default_version(ApiVersion::V2)
.supported_versions([ApiVersion::V1, ApiVersion::V2, ApiVersion::V3])
.deprecated(ApiVersion::V1)
.require_version(false)
.add_response_headers(true);
Resolving Version from Request
async fn handle(req: HttpRequest, config: &VersionConfig) -> Result<HttpResponse, Error> {
// Resolve version (uses default if not provided)
let version = config.resolve_version(&req)?;
// Process request...
let response = process(&req, &version)?;
// Apply version headers
Ok(config.apply_headers(response, &version))
}
Configuration Options
| Option | Description |
|---|---|
default_version |
Version to use when none provided |
supported_versions |
List of valid versions |
deprecated |
Mark version as deprecated |
require_version |
Fail if version not provided |
add_response_headers |
Add version info to responses |
Versioned Handlers
Route to different handlers based on version:
use armature_core::versioning::{VersionedHandler, ApiVersion};
// Create versioned handler registry
let handler = VersionedHandler::new()
.version(ApiVersion::V1, handle_users_v1)
.version(ApiVersion::V2, handle_users_v2)
.version(ApiVersion::V3, handle_users_v3)
.fallback(handle_users_latest);
// Get handler for version
if let Some(handler) = handler.get(&version) {
handler(req).await
}
// Or get compatible handler (same major version)
if let Some(handler) = handler.get_compatible(&version) {
handler(req).await
}
Version-Specific Route Implementation
use armature_core::{HttpRequest, HttpResponse, Error};
// V1 implementation
async fn get_users_v1(req: HttpRequest) -> Result<HttpResponse, Error> {
// Simple user list
let users: Vec<UserV1> = db.get_users().await?;
HttpResponse::ok().with_json(&users)
}
// V2 implementation with pagination
async fn get_users_v2(req: HttpRequest) -> Result<HttpResponse, Error> {
let page = req.query("page")
.and_then(|p| p.parse().ok())
.unwrap_or(1);
let users: PaginatedResult<UserV2> = db.get_users_paginated(page).await?;
HttpResponse::ok().with_json(&users)
}
// V3 implementation with cursor-based pagination
async fn get_users_v3(req: HttpRequest) -> Result<HttpResponse, Error> {
let cursor = req.query("cursor");
let result: CursorResult<UserV3> = db.get_users_cursor(cursor).await?;
HttpResponse::ok().with_json(&result)
}
Request and Response Extensions
VersionedRequest Trait
pub trait VersionedRequest {
/// Extract version using strategy
fn api_version(&self, strategy: &VersioningStrategy) -> Option<ApiVersion>;
/// Extract from URL path
fn url_version(&self) -> Option<ApiVersion>;
/// Extract from header
fn header_version(&self, header_name: &str) -> Option<ApiVersion>;
/// Extract from query parameter
fn query_version(&self, param_name: &str) -> Option<ApiVersion>;
}
VersionedResponse Trait
pub trait VersionedResponse {
/// Add X-API-Version header
fn with_api_version(self, version: ApiVersion) -> Self;
/// Add deprecation warning headers
fn with_deprecation_warning(self, message: &str) -> Self;
/// Add X-API-Supported-Versions header
fn with_supported_versions(self, versions: &[ApiVersion]) -> Self;
/// Add Sunset header (RFC 8594)
fn with_sunset_date(self, date: &str) -> Self;
}
Response Headers
When add_response_headers is enabled:
HTTP/1.1 200 OK
X-API-Version: 2
X-API-Supported-Versions: 1, 2, 3
For deprecated versions:
HTTP/1.1 200 OK
X-API-Version: 1
X-API-Deprecated: true
X-API-Deprecation-Message: API version 1 is deprecated
Deprecation: true
With sunset date:
HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
Migration Strategies
Gradual Deprecation
- Announce deprecation (add warning headers)
- Set sunset date (RFC 8594)
- Monitor usage (log deprecated version calls)
- Remove support (return 410 Gone)
let config = VersionConfig::new(VersioningStrategy::url_path())
.supported_versions([ApiVersion::V1, ApiVersion::V2, ApiVersion::V3])
.deprecated(ApiVersion::V1);
async fn handle(req: HttpRequest) -> Result<HttpResponse, Error> {
let version = config.resolve_version(&req)?;
let response = process(&req, &version)?;
// Add sunset date for v1
if version == ApiVersion::V1 {
return Ok(response
.with_api_version(version)
.with_deprecation_warning("v1 will be removed on 2025-01-01")
.with_sunset_date("Sat, 01 Jan 2025 00:00:00 GMT"));
}
Ok(config.apply_headers(response, &version))
}
Version Compatibility
use armature_core::versioning::VersionConstraint;
// Accept any v2.x
let constraint = VersionConstraint::range(
ApiVersion::new(2, 0),
ApiVersion::new(2, 99),
);
if constraint.matches(&requested_version) {
// Handle request
}
Best Practices
1. Choose the Right Strategy
| Strategy | Pros | Cons | Use When |
|---|---|---|---|
| URL Path | Visible, cacheable | URL pollution | Public APIs |
| Header | Clean URLs | Less discoverable | Internal APIs |
| Query Param | Easy to test | Can be stripped | Development |
| Media Type | RESTful | Complex | HATEOAS APIs |
2. Start with URL Path
// Most widely understood
let strategy = VersioningStrategy::url_path();
// URLs like: /v1/users, /v2/users
3. Always Set a Default Version
let config = VersionConfig::new(strategy)
.default_version(ApiVersion::V1); // Don't break existing clients
4. Document Deprecation Timeline
fn deprecated_response(response: HttpResponse) -> HttpResponse {
response
.with_deprecation_warning("v1 deprecated since 2024-01-01")
.with_sunset_date("Sat, 01 Jul 2024 00:00:00 GMT")
}
5. Use Semantic Versioning
// Major.Minor format
let version = ApiVersion::new(2, 1); // v2.1
// Breaking changes โ increment major
// Non-breaking additions โ increment minor
6. Maintain Version Parity
Keep similar functionality across versions when possible:
// Both versions return users, just different formats
async fn get_users_v1(req: HttpRequest) -> Result<HttpResponse, Error> { ... }
async fn get_users_v2(req: HttpRequest) -> Result<HttpResponse, Error> { ... }
7. Version Your Data Models
// Separate models per version
mod v1 {
#[derive(Serialize)]
pub struct User {
pub id: u64,
pub name: String,
}
}
mod v2 {
#[derive(Serialize)]
pub struct User {
pub id: u64,
pub first_name: String,
pub last_name: String,
pub email: String,
}
}
Common Pitfalls
โ Breaking Changes in Minor Versions
// Bad: Breaking change in v1.1
let v1_0 = ApiVersion::new(1, 0);
let v1_1 = ApiVersion::new(1, 1); // Changed field name - breaking!
// Good: Use v2 for breaking changes
let v2 = ApiVersion::new(2, 0);
โ No Default Version
// Bad: Fails for clients without version
let config = VersionConfig::new(strategy)
.require_version(true); // Forces all clients to upgrade
// Good: Graceful fallback
let config = VersionConfig::new(strategy)
.default_version(ApiVersion::V1);
โ Removing Versions Too Quickly
// Bad: Sudden removal
supported_versions: [ApiVersion::V3] // V1, V2 clients break!
// Good: Gradual deprecation
supported_versions: [ApiVersion::V1, ApiVersion::V2, ApiVersion::V3]
deprecated: [ApiVersion::V1] // Warn first
API Reference
Types
| Type | Description |
|---|---|
ApiVersion |
Version number (major.minor) |
VersioningStrategy |
Strategy for extracting version |
VersionConfig |
Configuration for version handling |
VersionConstraint |
Version matching rules |
VersionedHandler<T> |
Version-specific handler registry |
VersionParseError |
Error parsing version string |
Constants
ApiVersion::V1 // Version 1.0
ApiVersion::V2 // Version 2.0
ApiVersion::V3 // Version 3.0
Traits
| Trait | Description |
|---|---|
VersionedRequest |
Request version extraction methods |
VersionedResponse |
Response version header methods |
Summary
Key Points:
- Choose a strategy - URL path is most common for public APIs
- Configure properly - Set default version and supported versions
- Handle deprecation gracefully - Use warning headers and sunset dates
- Version your models - Keep separate DTOs per version
- Document changes - Maintain clear changelog
Quick Reference:
use armature_core::versioning::{
ApiVersion, VersionConfig, VersioningStrategy,
VersionedRequest, VersionedResponse
};
// Setup
let config = VersionConfig::new(VersioningStrategy::url_path())
.default_version(ApiVersion::V1)
.supported_versions([ApiVersion::V1, ApiVersion::V2])
.deprecated(ApiVersion::V1);
// Extract version
let version = config.resolve_version(&req)?;
// Version-specific handling
match version.major {
1 => handle_v1(req).await,
2 => handle_v2(req).await,
_ => Err(Error::BadRequest("Unsupported version")),
}
// Add response headers
response.with_api_version(version)