There are several ways to implement API versioning, each with trade-offs in visibility, flexibility, and best practices. The most common types include URI path versioning, query parameters, headers, and content negotiation. Choosing the right method depends on your system’s complexity, consumer expectations, and how much control you need.
1. URL path versioning
In URL path versioning, the version number is included directly in the API endpoint’s path, making it highly visible and easy to manage via routing logic. It’s especially useful for public APIs or when working with external clients, as it clearly communicates which version is being used. It also works well with caching and documentation tools.
Example
Let’s say you run a product API for a retail platform. In version 1, the price is a single numeric field. In version 2, you introduce region-based pricing with currency and discount support.
// v1 response from GET /v1/products/123
{
"id": 123,
"name": "MacBook Air",
"price": 999
}
// v2 response from GET /v2/products/123
{
"id": 123,
"name": "MacBook Air",
"pricing": {
"USD": { "amount": 999, "discount": 50 },
"EUR": { "amount": 1099, "discount": 0 }
}
}
2. Query parameter versioning
In this method, version information is passed as a query string parameter. While this keeps the base URI consistent, it can make APIs less RESTful and pose issues with caching or link sharing if not handled properly. It’s better suited for internal APIs or scenarios where routing flexibility is critical.
Example
You're testing a new loyalty feature where product responses include a loyaltyPoints field—but only for beta users.
Client Request
GET /products/123?version=beta
Code Change (beta version):
{
"id": 123,
"name": "MacBook Air",
"price": 999,
"loyaltyPoints": 200
}
3. Header versioning
This approach uses custom headers—typically something like X-API-Version—to specify which version the client expects. The endpoint remains clean and RESTful, but the version is hidden from the URL, which may reduce discoverability and make debugging harder.
It's preferred when versioning needs to be abstracted away or when you’re dealing with sensitive APIs in enterprise environments.
Example
Your app is introducing GDPR support. In v2 of the user API, you stop sending email addresses by default for privacy.
Client Request
GET /users/123
Headers:
X-API-Version: 2
Code change (V1)
{
"id": 123,
"name": "Alice",
"email": "alice@example.com"
}
Code Change (V2)
{
"id": 123,
"name": "Alice"
// email omitted for privacy compliance
}
4. Content negotiation (Accept header)
With this method, the client specifies the desired version through a custom MIME type in the Accept header. This allows for highly granular control over both versioning and response format, making it great for APIs that evolve rapidly or return rich media.
Example
You're updating your payments API to return richer metadata, and you support both XML and JSON.
Client Request
GET /payments/987
Headers:
Accept: application/vnd.payments.v2+json
Code change (V1 json)
{
"id": 987,
"amount": 250,
"status": "completed"
}
Code Change (V2 json)
{
"id": 987,
"amount": 250,
"status": "completed",
"processedBy": "Stripe",
"timestamp": "2024-05-12T10:45:00Z"
}