API Versioning Strategy: URL, Header, and Content-Type Approaches with Deprecation
Design a robust API versioning strategy. Compare URL, header, and content-type versioning, implement deprecation policies, OpenAPI version management, and client migration guides.
API Versioning Strategy: URL, Header, and Content-Type Approaches with Deprecation
API versioning is a promise to your consumers: I will change this API, but I will give you time to adapt. Without a versioning strategy, every breaking change is an incident. With one, breaking changes are scheduled events with migration paths.
The choice of versioning mechanism (URL path, headers, content negotiation) matters less than the discipline of the process: what constitutes a breaking change, how long old versions are supported, how deprecation notices are communicated, and how clients migrate. This post covers all of it.
What Counts as a Breaking Change
Understanding breaking vs. non-breaking changes is the foundation of any versioning strategy:
| Change | Breaking? | Notes |
|---|---|---|
| Remove a field from response | โ Yes | Clients may depend on it |
| Rename a field | โ Yes | Old field name no longer works |
| Change field type (string โ number) | โ Yes | Parsing will fail |
| Change required โ optional field | โ No | Backward compatible |
| Add a new optional request field | โ No | Old clients ignore it |
| Add a new response field | โ No | Old clients ignore it |
| Change HTTP status code | โ Yes | Error handling logic breaks |
| Add new enum value | โ ๏ธ Maybe | If client validates exhaustively, it breaks |
| Remove an endpoint | โ Yes | Calls will 404 |
| Change validation rules (stricter) | โ Yes | Requests that worked now fail |
| Change validation rules (looser) | โ No | Previously rejected requests now work |
| Add required auth to previously open endpoint | โ Yes | 401 where clients expect 200 |
Versioning Approach Comparison
| Approach | Example | Pros | Cons |
|---|---|---|---|
| URL path | /v1/orders | Obvious, easy to test in browser, cacheable | Pollutes URL space, duplicates routes |
| Query parameter | /orders?version=1 | No route duplication | Inconsistent (often forgotten), poor caching |
| Custom header | API-Version: 2026-01 | Clean URLs, no duplication | Less discoverable, harder to test in browser |
| Accept header | Accept: application/vnd.myapp.v2+json | REST-pure, content negotiation | Complex to implement and document |
| Date-based | Stripe-Version: 2024-06-20 | Precise, tied to changelog | Client must know specific dates |
Recommendation for most teams: URL path versioning for public-facing APIs (simple, discoverable, works with every HTTP client), date-based header versioning for API products with many integrations (Stripe's approach โ every client pins to a specific version date).
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
URL Path Versioning with Fastify
// src/api/v1/orders.ts
import type { FastifyPluginAsync } from 'fastify';
export const ordersV1: FastifyPluginAsync = async (app) => {
app.get<{ Params: { id: string } }>('/orders/:id', async (req, reply) => {
const order = await orderService.findById(req.params.id);
// V1 response shape
return reply.send({
id: order.id,
status: order.status,
total: order.total,
created_at: order.createdAt.toISOString(), // snake_case in V1
});
});
};
// src/api/v2/orders.ts
export const ordersV2: FastifyPluginAsync = async (app) => {
app.get<{ Params: { id: string } }>('/orders/:id', async (req, reply) => {
const order = await orderService.findById(req.params.id);
// V2 response shape: camelCase, richer data
return reply.send({
id: order.id,
status: order.status,
subtotal: order.subtotal,
tax: order.tax,
total: order.total,
currency: order.currency,
createdAt: order.createdAt.toISOString(), // camelCase in V2
customer: {
id: order.customerId,
email: order.customerEmail,
},
});
});
};
// src/server.ts
app.register(ordersV1, { prefix: '/v1' });
app.register(ordersV2, { prefix: '/v2' });
Shared Service Layer (Never Duplicate Business Logic)
// src/services/orders.ts โ single source of truth
export class OrderService {
async findById(id: string): Promise<OrderEntity> {
return db.order.findUniqueOrThrow({
where: { id },
include: { customer: true, items: true },
});
}
// ... all business logic here, version-agnostic
}
// Route handlers only handle shape transformation
// Service layer handles business logic
// This is critical: don't duplicate business logic per version
Date-Based Header Versioning (Stripe Pattern)
// src/middleware/version.ts
const SUPPORTED_VERSIONS = [
'2026-01-01',
'2026-04-15',
'2026-07-01', // Current
] as const;
type ApiVersion = typeof SUPPORTED_VERSIONS[number];
const CURRENT_VERSION: ApiVersion = '2026-07-01';
const SUNSET_VERSIONS: Record<string, Date> = {
'2026-01-01': new Date('2027-01-01'), // Sunset Jan 2027
'2026-04-15': new Date('2027-07-01'),
};
export async function versionMiddleware(
request: FastifyRequest,
reply: FastifyReply,
): Promise<void> {
const requestedVersion = request.headers['myapp-version'] as string | undefined;
// Default to current version if not specified
if (!requestedVersion) {
request.apiVersion = CURRENT_VERSION;
reply.header('MyApp-Version', CURRENT_VERSION);
return;
}
// Validate version format and existence
if (!SUPPORTED_VERSIONS.includes(requestedVersion as ApiVersion)) {
reply.status(400).send({
error: 'invalid_api_version',
message: `API version '${requestedVersion}' is not supported.`,
supported_versions: SUPPORTED_VERSIONS,
current_version: CURRENT_VERSION,
});
return;
}
// Warn if version is deprecated (within 90 days of sunset)
const sunsetDate = SUNSET_VERSIONS[requestedVersion];
if (sunsetDate) {
const daysToSunset = Math.ceil(
(sunsetDate.getTime() - Date.now()) / 86400000,
);
reply.header('Deprecation', `true`);
reply.header('Sunset', sunsetDate.toUTCString());
reply.header('Link', `<https://docs.myapp.com/api/migration/${requestedVersion}>; rel="successor-version"`);
if (daysToSunset < 30) {
reply.header('Warning', `299 - "API version ${requestedVersion} sunsets in ${daysToSunset} days"`);
}
}
request.apiVersion = requestedVersion as ApiVersion;
reply.header('MyApp-Version', requestedVersion);
}
// Augment FastifyRequest type
declare module 'fastify' {
interface FastifyRequest {
apiVersion: ApiVersion;
}
}
Version-Aware Route Handler
// src/routes/orders.ts
app.get('/orders/:id', async (req, reply) => {
const order = await orderService.findById(req.params.id);
// Transform response based on API version
const response = req.apiVersion >= '2026-07-01'
? transformOrderV3(order) // Latest: includes fulfillment data
: req.apiVersion >= '2026-04-15'
? transformOrderV2(order) // V2: camelCase, customer embed
: transformOrderV1(order); // V1: snake_case, minimal fields
return reply.send(response);
});
function transformOrderV1(order: OrderEntity) {
return {
id: order.id,
status: order.status,
total: order.total,
created_at: order.createdAt.toISOString(),
};
}
function transformOrderV2(order: OrderEntity) {
return {
id: order.id,
status: order.status,
subtotal: order.subtotal,
tax: order.tax,
total: order.total,
currency: order.currency,
createdAt: order.createdAt.toISOString(),
customer: { id: order.customerId, email: order.customerEmail },
};
}
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
OpenAPI: Multi-Version Documentation
# openapi/v2.yaml
openapi: 3.1.0
info:
title: MyApp API
version: "2.0.0"
description: |
## API Versioning
This is **API v2**. See also:
- [API v1 documentation](/docs/api/v1) โ supported until 2027-01-01
## Breaking Changes from v1
- All field names changed from `snake_case` to `camelCase`
- `created_at` โ `createdAt`
- Response now includes nested `customer` object
- New required field `currency` on all money amounts
x-api-version: "2026-07-01"
contact:
email: api-support@myapp.com
url: https://docs.myapp.com
servers:
- url: https://api.myapp.com/v2
description: Production
- url: https://api.staging.myapp.com/v2
description: Staging
paths:
/orders/{id}:
get:
summary: Get order by ID
deprecated: false
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
responses:
"200":
description: Order details
content:
application/json:
schema:
$ref: "#/components/schemas/OrderV2"
"404":
$ref: "#/components/responses/NotFound"
components:
schemas:
OrderV2:
type: object
required: [id, status, total, currency, createdAt]
properties:
id:
type: string
format: uuid
status:
type: string
enum: [pending, confirmed, shipped, delivered, cancelled]
subtotal:
type: number
format: float
tax:
type: number
format: float
total:
type: number
format: float
currency:
type: string
pattern: "^[A-Z]{3}$"
example: "USD"
createdAt:
type: string
format: date-time
customer:
$ref: "#/components/schemas/CustomerEmbed"
Deprecation Policy and Communication
Standard Deprecation Timeline
API Deprecation Policy
Timeline
-
Announce โ 6 months before sunset:
- Blog post with migration guide
- Email to all API consumers
DeprecationandSunsetheaders added to responses
-
Warning period โ 90 days before sunset:
Warning: 299header added- Increased monitoring for traffic on old version
- Proactive outreach to high-traffic consumers
-
Final warning โ 30 days before sunset:
- Weekly emails to consumers still on old version
- Dashboard notification for active API keys
-
Sunset โ Old version returns 410 Gone:
- Response: { "error": "version_sunset", "migrate_to": "v2", "docs": "..." }
- Keep 410 for 30 days, then remove endpoint entirely
What's Never a Breaking Change
- Adding new optional fields to responses
- Adding new optional request fields
- Adding new endpoints
- Adding new enum values (document that clients should handle unknown values)
- Improving error messages (same error code, better message)
### Programmatic Deprecation Notice (Response Header)
```typescript
// Sunset header middleware (RFC 8594)
function addDeprecationHeaders(
reply: FastifyReply,
version: string,
sunsetDate: Date,
successorUrl: string,
): void {
reply.headers({
'Deprecation': 'true',
'Sunset': sunsetDate.toUTCString(), // RFC 8594
'Link': `<${successorUrl}>; rel="successor-version"`,
'Warning': `299 api.myapp.com "Sunset: ${sunsetDate.toDateString()}"`,
});
}
Version Sunset: 410 Gone Handler
// When a version is fully sunset
app.register(async (app) => {
app.addHook('onRequest', async (req, reply) => {
reply.status(410).send({
error: 'version_sunset',
message: 'API v1 was sunset on January 1, 2027. Please migrate to v2.',
documentation: 'https://docs.myapp.com/api/migration/v1-to-v2',
current_version: 'https://api.myapp.com/v2',
});
});
}, { prefix: '/v1' });
Client SDK Versioning
For teams that publish SDKs, use semantic versioning with explicit API version pinning:
// SDK initialization: pin API version
const client = new MyAppClient({
apiKey: 'sk_live_...',
apiVersion: '2026-07-01', // Explicitly pin; never silently upgrade
// SDK major version bump when minimum supported API version changes
});
Working With Viprasol
We design and implement API versioning strategies for SaaS products โ from initial versioning architecture through deprecation policies and client migration tooling.
What we deliver:
- Versioning strategy selection with rationale for your use case
- Fastify/Express middleware for header-based versioning
- OpenAPI spec management (separate specs per major version)
- Deprecation notice implementation (Sunset/Deprecation headers, RFC 8594)
- Migration guides and changelog automation
โ Discuss your API architecture โ Web development and API services
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.