
Api Versioning Strategy
- 419 installs
- 305 repo stars
- Updated March 4, 2026
- aj-geddes/useful-ai-prompts
api-versioning-strategy is an agent skill that implements API versioning schemes, backward-compatibility rules, deprecation timelines, and client migration guides for developers managing breaking changes across REST, Gra
About
api-versioning-strategy is a useful-ai-prompts skill with reference guides on versioning approaches, backward-compatibility patterns, deprecation strategy, migration examples, and version detection routing for REST, GraphQL, and gRPC APIs. It includes Express and TypeScript quick-start examples for /api/v1 and /api/v2 routes with version-specific response transformers and recommends versioning from day one, supporting at least two simultaneous versions, and giving consumers 6–12 months deprecation notice. Developers reach for api-versioning-strategy when adding breaking fields, deprecating endpoints, writing migration documentation, or choosing between URL, header, and content-negotiation versioning before shipping API changes.
- version schemes
- deprecation policy
- backward compatibility
- client migration
- changelog discipline
Api Versioning Strategy by the numbers
- 419 all-time installs (skills.sh)
- Ranked #1,050 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aj-geddes/useful-ai-prompts --skill api-versioning-strategyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 419 |
|---|---|
| repo stars | ★ 305 |
| Last updated | March 4, 2026 |
| Repository | aj-geddes/useful-ai-prompts ↗ |
How do you version REST APIs without breaking clients?
Define URL, header, or media-type versioning schemes, deprecation timelines, backward-compatibility rules, and client migration plans before breaking API changes ship.
Who is it for?
API developers designing versioned REST, GraphQL, or gRPC services who need backward compatibility and documented client migration paths.
Skip if: Internal-only prototypes with no external consumers where breaking changes can ship without version negotiation or deprecation policy.
When should I use this skill?
The task involves API versions, deprecating endpoints, managing breaking changes, or implementing version routing across supported releases.
What you get
A versioning scheme document, route or adapter patterns, deprecation timeline, migration guide with code examples, and multi-version test plan.
- versioning scheme specification
- migration guide
- multi-version routing implementation
By the numbers
- Includes 12 reference guides in the references directory
- Recommends 6–12 months deprecation notice for API consumers
- Advises supporting at least 2 API versions simultaneously
Files
API Versioning Strategy
Table of Contents
Overview
Comprehensive guide to API versioning approaches, deprecation strategies, backward compatibility techniques, and migration planning for REST APIs, GraphQL, and gRPC services.
When to Use
- Designing new APIs with versioning from the start
- Adding breaking changes to existing APIs
- Deprecating old API versions
- Planning API migrations
- Ensuring backward compatibility
- Managing multiple API versions simultaneously
- Creating API documentation for different versions
- Implementing API version routing
Quick Start
Minimal working example:
// express-router.ts
import express from "express";
const app = express();
// Version 1
app.get("/api/v1/users", (req, res) => {
res.json({
users: [{ id: 1, name: "John Doe" }],
});
});
// Version 2 - Added email field
app.get("/api/v2/users", (req, res) => {
res.json({
users: [{ id: 1, name: "John Doe", email: "john@example.com" }],
});
});
// Shared logic with version-specific transformations
app.get("/api/:version/users/:id", async (req, res) => {
const user = await userService.findById(req.params.id);
if (req.params.version === "v1") {
res.json({ id: user.id, name: user.name });
// ... (see reference guides for full implementation)Reference Guides
Detailed implementations in the references/ directory:
| Guide | Contents |
|---|---|
| Versioning Approaches | Versioning Approaches |
| Backward Compatibility Patterns | Backward Compatibility Patterns |
| Deprecation Strategy | Deprecation Strategy |
| Migration Guide Example | Migration Guide Example |
| Response Structure | Response Structure |
| Date Format | Date Format, Error Format |
| JavaScript/TypeScript | JavaScript/TypeScript, Python |
| GraphQL Versioning | GraphQL Versioning |
| gRPC Versioning | gRPC Versioning |
| Version Detection & Routing | Version Detection & Routing |
| Testing Multiple Versions | Testing Multiple Versions |
| Pattern 1: Version-Agnostic Core | Pattern 1: Version-Agnostic Core, Pattern 2: Feature Flags for Gradual Rollout, Pattern 3: API Version Metrics |
Best Practices
✅ DO
- Version from day one (even if v1)
- Document breaking vs non-breaking changes
- Provide clear migration guides with code examples
- Use semantic versioning principles
- Give 6-12 months deprecation notice
- Monitor usage of deprecated APIs
- Send deprecation warnings to API consumers
- Support at least 2 versions simultaneously
- Use adapters/transformers for version logic
- Test all supported versions
- Log which API version is being used
- Provide migration tooling when possible
- Be consistent with versioning approach
❌ DON'T
- Change API behavior without versioning
- Remove versions without notice
- Support too many versions (>3)
- Use different versioning strategies in same API
- Break APIs without incrementing version
- Forget to update documentation
- Deprecate too quickly (<6 months)
- Ignore feedback from API consumers
- Make every change a new version
- Use version numbers inconsistently
Backward Compatibility Patterns
Backward Compatibility Patterns
Additive Changes (Non-Breaking)
// ✅ Safe: Adding optional fields
interface UserV1 {
id: string;
name: string;
}
interface UserV2 extends UserV1 {
email?: string; // Optional field
avatar?: string; // Optional field
}
// ✅ Safe: Adding new endpoints
app.post("/api/v1/users/:id/avatar", uploadAvatar);
// ✅ Safe: Accepting additional parameters
app.get("/api/v1/users", (req, res) => {
const { page, limit, sortBy } = req.query; // New optional params
const users = await userService.list({ page, limit, sortBy });
res.json(users);
});Breaking Changes (Require New Version)
// ❌ Breaking: Removing fields
interface UserV1 {
id: string;
name: string;
username: string;
}
interface UserV2 {
id: string;
name: string;
// username removed - BREAKING!
}
// ❌ Breaking: Changing field types
interface UserV1 {
id: string;
created: string; // ISO string
}
interface UserV2 {
id: string;
created: number; // Unix timestamp - BREAKING!
}
// ❌ Breaking: Renaming fields
interface UserV1 {
fullName: string;
}
interface UserV2 {
name: string; // Renamed from fullName - BREAKING!
}
// ❌ Breaking: Changing response structure
// V1
{ users: [...], total: 10 }
// V2 - BREAKING!
{ data: [...], meta: { total: 10 } }Handling Both Versions
// version-adapter.ts
export class UserAdapter {
toV1(user: User): UserV1Response {
return {
id: user.id,
name: user.fullName,
username: user.username,
created: user.createdAt.toISOString(),
};
}
toV2(user: User): UserV2Response {
return {
id: user.id,
name: user.fullName,
email: user.email,
profile: {
avatar: user.avatarUrl,
bio: user.bio,
},
createdAt: user.createdAt.getTime(),
};
}
fromV1(data: UserV1Request): User {
return {
fullName: data.name,
username: data.username,
email: data.email || null,
};
}
fromV2(data: UserV2Request): User {
return {
fullName: data.name,
username: data.username || generateUsername(data.email),
email: data.email,
avatarUrl: data.profile?.avatar,
bio: data.profile?.bio,
};
}
}
// Usage in controller
app.get("/api/:version/users/:id", async (req, res) => {
const user = await userService.findById(req.params.id);
const adapter = new UserAdapter();
const response =
req.params.version === "v2" ? adapter.toV2(user) : adapter.toV1(user);
res.json(response);
});Date Format
Date Format
v1: ISO 8601 strings v2: Unix timestamps
Migration:
// Before
const created = new Date(user.created);
// After
const created = new Date(user.created * 1000);Error Format
v1:
{ "error": "User not found" }v2:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found",
"details": {}
}
}Deprecation Strategy
Deprecation Strategy
Deprecation Headers
// deprecation-middleware.ts
export function deprecationWarning(version: string, sunsetDate: Date) {
return (req, res, next) => {
res.setHeader("Deprecation", "true");
res.setHeader("Sunset", sunsetDate.toUTCString());
res.setHeader("Link", '</api/v2/docs>; rel="successor-version"');
res.setHeader(
"X-API-Warn",
`Version ${version} is deprecated. Please migrate to v2 by ${sunsetDate.toDateString()}`,
);
next();
};
}
// Apply to deprecated routes
app.use("/api/v1/*", deprecationWarning("v1", new Date("2024-12-31")));
app.get("/api/v1/users", (req, res) => {
// Return v1 response with deprecation headers
res.json(users);
});Deprecation Response
// Include deprecation info in response body
app.get('/api/v1/users', (req, res) => {
res.json({
_meta: {
deprecated: true,
sunsetDate: '2024-12-31',
message: 'This API version is deprecated. Please migrate to v2.',
migrationGuide: 'https://docs.example.com/migration-v1-to-v2'
},
users: [...]
});
});Gradual Deprecation Timeline
// deprecation-stages.ts
enum DeprecationStage {
SUPPORTED = "supported",
DEPRECATED = "deprecated",
SUNSET_ANNOUNCED = "sunset_announced",
READONLY = "readonly",
SHUTDOWN = "shutdown",
}
const versionStatus = {
v1: {
stage: DeprecationStage.READONLY,
sunsetDate: new Date("2024-06-30"),
message: "Read-only mode. New writes are disabled.",
},
v2: {
stage: DeprecationStage.DEPRECATED,
sunsetDate: new Date("2024-12-31"),
message: "Deprecated. Please migrate to v3.",
},
v3: {
stage: DeprecationStage.SUPPORTED,
message: "Current stable version.",
},
};
// Middleware to enforce deprecation
app.use("/api/:version/*", (req, res, next) => {
const status = versionStatus[req.params.version];
if (!status) {
return res.status(404).json({ error: "API version not found" });
}
if (status.stage === DeprecationStage.SHUTDOWN) {
return res.status(410).json({ error: "API version no longer available" });
}
if (
status.stage === DeprecationStage.READONLY &&
["POST", "PUT", "PATCH", "DELETE"].includes(req.method)
) {
return res.status(403).json({
error: "API version is read-only",
message: status.message,
});
}
// Add deprecation headers
if (status.stage !== DeprecationStage.SUPPORTED) {
res.setHeader("X-API-Deprecated", "true");
res.setHeader("X-API-Sunset", status.sunsetDate.toISOString());
}
next();
});GraphQL Versioning
GraphQL Versioning
// GraphQL handles versioning differently - through schema evolution
// schema-v1.graphql
type User {
id: ID!
name: String!
username: String!
}
// schema-v2.graphql (deprecated fields)
type User {
id: ID!
name: String!
username: String! @deprecated(reason: "Use email instead")
email: String!
profile: Profile
}
type Profile {
avatar: String
bio: String
}
// Field deprecation in resolver
const resolvers = {
User: {
username: (user) => {
console.warn('username field is deprecated, use email instead');
return user.email;
}
}
};gRPC Versioning
gRPC Versioning
// v1/user.proto
syntax = "proto3";
package user.v1;
message User {
string id = 1;
string name = 2;
}
// v2/user.proto
syntax = "proto3";
package user.v2;
message User {
string id = 1;
string name = 2;
string email = 3;
Profile profile = 4;
}
message Profile {
string avatar = 1;
string bio = 2;
}
// Both versions can coexist
service UserServiceV1 {
rpc GetUser (GetUserRequest) returns (user.v1.User);
}
service UserServiceV2 {
rpc GetUser (GetUserRequest) returns (user.v2.User);
}JavaScript/TypeScript
JavaScript/TypeScript
// v1 Client
class ApiClientV1 {
async getUsers() {
const response = await fetch("/api/v1/users");
const data = await response.json();
return data.users;
}
}
// v2 Client
class ApiClientV2 {
async getUsers() {
const response = await fetch("/api/v2/users");
const data = await response.json();
return data.data; // Changed from .users to .data
}
}Python
# v1
response = requests.get(f"{base_url}/api/v1/users")
users = response.json()["users"]
# v2
response = requests.get(f"{base_url}/api/v2/users")
users = response.json()["data"]````
Migration Guide Example
Migration Guide Example
````markdown
API Migration Guide: v1 to v2
Pattern 1: Version-Agnostic Core
Pattern 1: Version-Agnostic Core
// Core logic remains version-agnostic
class UserService {
async getUser(id: string): Promise<User> {
return this.repository.findById(id);
}
}
// Version-specific adapters
class UserV1Adapter {
transform(user: User): UserV1 {
/* ... */
}
}
class UserV2Adapter {
transform(user: User): UserV2 {
/* ... */
}
}Pattern 2: Feature Flags for Gradual Rollout
app.get("/api/v2/users", async (req, res) => {
const user = await userService.getUser(req.params.id);
// Gradual rollout of new feature
if (featureFlags.isEnabled("enhanced-profile", req.user.id)) {
return res.json(transformWithEnhancedProfile(user));
}
return res.json(transformV2(user));
});Pattern 3: API Version Metrics
// Track usage by version
app.use((req, res, next) => {
const version = detectVersion(req);
metrics.increment("api.requests", { version });
next();
});Response Structure
Response Structure
v1:
{
"users": [...],
"total": 10,
"page": 1
}````
v2:
{
"data": [...],
"meta": {
"total": 10,
"page": 1,
"perPage": 20
}
}Migration:
// Before
const users = response.users;
const total = response.total;
// After
const users = response.data;
const total = response.meta.total;Testing Multiple Versions
Testing Multiple Versions
// api-version.test.ts
describe("API Versioning", () => {
describe("v1", () => {
it("should return user with v1 format", async () => {
const response = await request(app).get("/api/v1/users/1").expect(200);
expect(response.body).toHaveProperty("id");
expect(response.body).toHaveProperty("name");
expect(response.body).not.toHaveProperty("email");
});
});
describe("v2", () => {
it("should return user with v2 format", async () => {
const response = await request(app).get("/api/v2/users/1").expect(200);
expect(response.body).toHaveProperty("id");
expect(response.body).toHaveProperty("name");
expect(response.body).toHaveProperty("email");
expect(response.body).toHaveProperty("profile");
});
it("should include deprecation headers for v1", async () => {
const response = await request(app).get("/api/v1/users/1");
expect(response.headers["deprecation"]).toBe("true");
expect(response.headers["sunset"]).toBeDefined();
});
});
describe("version negotiation", () => {
it("should use version from header", async () => {
const response = await request(app)
.get("/api/users/1")
.set("API-Version", "2")
.expect(200);
expect(response.body).toHaveProperty("email");
});
it("should default to v1 if no version specified", async () => {
const response = await request(app).get("/api/users/1").expect(200);
expect(response.body).not.toHaveProperty("email");
});
});
});Version Detection & Routing
Version Detection & Routing
// version-router.ts
import express from "express";
export class VersionRouter {
private versions = new Map<string, express.Router>();
registerVersion(version: string, router: express.Router) {
this.versions.set(version, router);
}
getMiddleware() {
return (req, res, next) => {
// Detect version from multiple sources
const version = this.detectVersion(req);
const router = this.versions.get(version);
if (!router) {
return res.status(400).json({
error: "Invalid API version",
supportedVersions: Array.from(this.versions.keys()),
});
}
// Set version in request for logging
req.apiVersion = version;
// Use versioned router
router(req, res, next);
};
}
private detectVersion(req): string {
// 1. Check URL path
const pathMatch = req.path.match(/^\/api\/v(\d+)\//);
if (pathMatch) return pathMatch[1];
// 2. Check header
if (req.headers["api-version"]) {
return req.headers["api-version"];
}
// 3. Check Accept header
const acceptMatch = req.headers["accept"]?.match(
/application\/vnd\.myapi\.v(\d+)\+json/,
);
if (acceptMatch) return acceptMatch[1];
// 4. Check query parameter
if (req.query.version) {
return req.query.version;
}
// 5. Default version
return "1";
}
}
// Usage
const versionRouter = new VersionRouter();
versionRouter.registerVersion("1", v1Router);
versionRouter.registerVersion("2", v2Router);
versionRouter.registerVersion("3", v3Router);
app.use("/api", versionRouter.getMiddleware());Versioning Approaches
Versioning Approaches
URL Path Versioning
// express-router.ts
import express from "express";
const app = express();
// Version 1
app.get("/api/v1/users", (req, res) => {
res.json({
users: [{ id: 1, name: "John Doe" }],
});
});
// Version 2 - Added email field
app.get("/api/v2/users", (req, res) => {
res.json({
users: [{ id: 1, name: "John Doe", email: "john@example.com" }],
});
});
// Shared logic with version-specific transformations
app.get("/api/:version/users/:id", async (req, res) => {
const user = await userService.findById(req.params.id);
if (req.params.version === "v1") {
res.json({ id: user.id, name: user.name });
} else if (req.params.version === "v2") {
res.json({ id: user.id, name: user.name, email: user.email });
}
});Pros: Simple, explicit, cache-friendly Cons: URL pollution, harder to deprecate
Header Versioning (Content Negotiation)
// header-versioning.ts
app.get("/api/users", (req, res) => {
const version = req.headers["api-version"] || "1";
switch (version) {
case "1":
return res.json(transformToV1(users));
case "2":
return res.json(transformToV2(users));
default:
return res.status(400).json({ error: "Unsupported API version" });
}
});
// Or using Accept header
app.get("/api/users", (req, res) => {
const acceptHeader = req.headers["accept"];
if (acceptHeader.includes("application/vnd.myapi.v2+json")) {
return res.json(transformToV2(users));
}
// Default to v1
return res.json(transformToV1(users));
});Pros: Clean URLs, RESTful Cons: Less visible, harder to test manually
Query Parameter Versioning
// query-param-versioning.ts
app.get("/api/users", (req, res) => {
const version = req.query.version || "1";
if (version === "2") {
return res.json(transformToV2(users));
}
return res.json(transformToV1(users));
});
// Usage: GET /api/users?version=2Pros: Easy to implement, flexible Cons: Not RESTful, can be overlooked
#!/bin/bash
# validate-api.sh - Validate API specification
# Usage: ./validate-api.sh <openapi_spec>
set -euo pipefail
SPEC_FILE="${{1:?Usage: $0 <openapi_spec>}}"
echo "Validating API spec: $SPEC_FILE"
# TODO: Add API validation
# - Validate OpenAPI/Swagger syntax
# - Check endpoint naming conventions
# - Verify response schemas
# - Check for required headers
# - Validate authentication definitions
echo "API validation complete."
# API Endpoint Scaffold
# TODO: Customize for your API framework
openapi: "3.0.3"
info:
title: "API Service"
version: "1.0.0"
paths:
/api/v1/resource:
get:
summary: "List resources"
# TODO: Define parameters and responses
responses:
"200":
description: "Success"
post:
summary: "Create resource"
# TODO: Define request body and responses
responses:
"201":
description: "Created"
Related skills
How it compares
Choose api-versioning-strategy when external API consumers need documented migration paths rather than ad-hoc breaking endpoint edits.
FAQ
Which versioning approaches does api-versioning-strategy cover?
api-versioning-strategy documents URL path versioning, header-based versioning, media-type negotiation, plus GraphQL and gRPC-specific patterns. Reference guides cover version detection, routing, and testing across simultaneously supported releases.
What deprecation timeline does api-versioning-strategy recommend?
api-versioning-strategy recommends giving API consumers 6–12 months deprecation notice, monitoring usage of deprecated endpoints, sending deprecation warnings, and supporting at least two versions simultaneously before removal.
Does api-versioning-strategy include code examples?
api-versioning-strategy provides a TypeScript Express quick start with separate /api/v1 and /api/v2 routes, shared service logic, and version-specific response transformers, plus additional patterns in the references directory.