Actionable rules and patterns to design, build, test, deploy, and retire multiple API versions with zero-downtime across REST and GraphQL services.
You're shipping API changes faster than ever, but every deployment brings that familiar anxiety: "Will this break existing clients?" The traditional approach of versioning as an afterthought has burned too many teams. It's time for a systematic approach that eliminates the fear of API evolution.
The Breaking Change Trap: You need to modify a response structure, but you have mobile apps in production that can't update immediately. Do you break existing clients or delay critical improvements?
The Maintenance Nightmare: You're maintaining three different API versions with inconsistent patterns, duplicated code, and no clear deprecation strategy. Each bug fix becomes a multi-version headache.
The Client Communication Breakdown: Clients discover breaking changes in production because your versioning strategy relies on hope and manual coordination rather than systematic contracts.
The Technical Debt Spiral: Every API decision becomes permanent because you lack the infrastructure to evolve gracefully, leading to increasingly complex workarounds.
These Cursor Rules establish a systematic approach to API versioning that treats version management as a core architectural concern, not an afterthought. You'll implement semantic versioning with automated contract testing, zero-downtime deployment strategies, and clear client migration paths.
The rules cover the complete versioning lifecycle: from initial design through deployment, maintenance, and eventual retirement of API versions.
Eliminate Production Surprises: Automated contract testing catches breaking changes before they reach clients. Your CI pipeline blocks deployments that would break existing integrations.
Reduce Maintenance Overhead by 60%: Structured version directories and shared handler patterns minimize code duplication while maintaining clear separation between versions.
Accelerate Feature Delivery: Deploy new API features immediately behind new versions while maintaining full backward compatibility for existing clients.
Streamline Client Communication: Automated documentation generation and deprecation headers give clients clear migration paths with predictable timelines.
// Scattered version logic throughout codebase
app.get('/users', (req, res) => {
if (req.query.version === '2') {
// Different response structure
res.json({ data: users, meta: { count: users.length }});
} else {
// Legacy format
res.json(users);
}
});
Problems: Version logic mixed with business logic, no contract enforcement, manual testing, unclear deprecation path.
// Clear version separation with contract validation
// src/v1/controllers/users.ts
export const getUsersV1 = async (req: Request, res: Response) => {
const users = await userService.getUsers();
res.json(users); // Contract-validated response
};
// src/v2/controllers/users.ts
export const getUsersV2 = async (req: Request, res: Response) => {
const users = await userService.getUsers();
res.json({
data: users,
meta: { count: users.length, version: 2 }
});
};
Results: Clean separation, automated validation, testable contracts, clear upgrade path.
// tests/contract/v1-v2-compatibility.test.ts
describe('V1 to V2 Migration', () => {
it('should maintain backward compatibility', async () => {
const v1Response = await request(app).get('/api/v1/users');
const v2Response = await request(app).get('/api/v2/users');
// Automated compatibility validation
expect(isBackwardCompatible(v1Response.body, v2Response.body.data)).toBe(true);
});
});
mkdir -p src/{v1,v2}/{controllers,routes,validators}
mkdir -p contracts/{v1,v2}
mkdir -p tests/{v1,v2}
// src/version-manager.ts
export class VersionManager {
private versions = new Map<number, Router>();
register(version: number, router: Router) {
if (this.versions.has(version)) {
throw new Error(`Version ${version} already registered`);
}
this.versions.set(version, router);
}
getRouter(version: number): Router | null {
return this.versions.get(version) || null;
}
}
// middleware/version-resolver.ts
export const resolveApiVersion = (req: Request, res: Response, next: NextFunction) => {
const headerVersion = req.get('Accept')?.match(/v=(\d+)/)?.[1];
const pathVersion = req.path.match(/\/v(\d+)\//)?.[1];
const queryVersion = req.query.v as string;
req.apiVersion = Number(headerVersion || pathVersion || queryVersion || 1);
if (!isVersionSupported(req.apiVersion)) {
return res.status(400).json({
error: 'INVALID_VERSION',
supported: getSupportedVersions(),
deprecated: getDeprecatedVersions()
});
}
next();
};
# contracts/v1/openapi.yml
openapi: 3.0.0
info:
version: "1.0.0"
title: User API V1
paths:
/users:
get:
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
# .github/workflows/api-versioning.yml
name: API Version Management
on: [push, pull_request]
jobs:
contract-validation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate OpenAPI Contracts
run: |
npx swagger-codegen validate -i contracts/v1/openapi.yml
npx swagger-codegen validate -i contracts/v2/openapi.yml
- name: Check Breaking Changes
run: |
npx openapi-diff contracts/v1/openapi.yml contracts/v2/openapi.yml
- name: Run Contract Tests
run: npm run test:contracts
Immediate Productivity Gains:
Long-term Architectural Benefits:
Team Workflow Improvements:
Your API evolution becomes predictable, your clients stay happy, and your team ships faster. The rules eliminate the guesswork from version management and establish patterns that scale with your API's growth.
Start with version 1 of your next API endpoint. Future you will thank present you for building version management in from day one.
You are an expert in REST & GraphQL API versioning on Node.js (TypeScript + Express/NestJS), Java (Spring Boot), Python (Django REST Framework), Go (Gin), and OpenAPI/Swagger tooling.
Key Principles
- Version from day-0; never publish an un-versioned endpoint.
- Semantic Versioning (MAJOR.MINOR.PATCH) governs change impact; only MAJOR affects routing.
- One breaking change ⇒ new MAJOR version; MINOR & PATCH remain fully backward compatible.
- Maintain at least two live MAJOR versions; deprecate the eldest only after ≥12 months’ notice.
- Prefer additive evolution (new fields, new endpoints) instead of destructive changes.
- Every versioned contract lives in source control (`/contracts/v{n}/openapi.yaml`).
- Change-log, migration guide, and sunset date must accompany every release.
- Use contract tests to enforce backward / forward compatibility before merge.
Language-Specific Rules
TypeScript / JavaScript (Node + Express/NestJS)
- Directory layout: `src/v{n}/routes`, `src/v{n}/controllers`, `src/v{n}/validators`.
- Expose a single registration file per version:
```ts
// src/v1/index.ts
export default function registerV1(app: Express) {
app.use('/api/v1', routes);
}
```
- Keep version constant: `export const API_VERSION = 1 as const;` — reference in tests & docs.
- Accept header-based versioning (`Accept: application/vnd.myapp+json;v=2`) via middleware:
```ts
app.use((req,res,next)=>{
req.apiVersion = Number(req.get('Accept')?.match(/v=(\d+)/)?.[1] || 1);
return next();
});
```
- Use TypeScript `never` type in discriminated unions to catch unhandled versions at compile time.
Java (Spring Boot)
- Annotate controllers:
```java
@RestController
@RequestMapping("/api/v{version}")
public class UserControllerV1 {
@GetMapping("/users")
public List<UserDto> getUsers(@PathVariable int version) { ... }
}
```
- Alternatively, implement header strategy with `@RequestHeader("API-Version")` and a `HandlerMapping`.
- Keep version constants in `ApiVersions.java` enum to prevent magic numbers.
Python (Django REST Framework)
- Set `DEFAULT_VERSIONING_CLASS = 'rest_framework.versioning.NamespaceVersioning'`.
- URLConf pattern: `path('api/v1/', include(('app.api.v1.urls', 'v1')))`. Namespaces guarantee resolver isolation.
- Unit tests: `self.client.get('/api/v2/users/', HTTP_ACCEPT='application/vnd.myapp; version=2')`.
Go (Gin)
- Register router groups:
```go
v1 := r.Group("/api/v1")
v2 := r.Group("/api/v2")
```
- Re-expose shared handlers via thin adapters to avoid code duplication.
- Compile-time flag `-X main.supportedVersions=\"1,2\"` surfaces valid versions in metrics.
Error Handling & Validation
- Reject unknown versions with `400 Bad Request` and machine-readable body `{code: "INVALID_VERSION", supported: [1,2]}`.
- Retired versions return `410 Gone` plus `Sunset` header and `Link: </docs/v2>; rel="latest"`.
- Validate request/response against version-specific OpenAPI schema using middleware (e.g., `express-openapi-validator`).
- Fast-fail rule: validate headers & path params before hitting controller logic; early `return` on error.
Framework-Specific Rules
Express.js
- Central `apiVersionRouter.ts` routes traffic by inspecting header, query (`?v=`), then path (fallback).
- Mount downstream routers lazily to minimise memory footprint for stale versions.
- Use `morgan` token `:api-version` to log version per request.
NestJS
- Leverage `VersioningType.URI` or `VersioningType.HEADER` via `app.enableVersioning()`.
- Each controller carries `@Version('1')` decorator; compose with `@Controller('users')`.
Spring Boot
- Put version into `info.version` in `application.yaml`; CI verifies that value matches latest contract folder.
- Define `ApiVersionCondition` to allow method-level version negotiation.
Django REST Framework
- Versioning class hierarchy: `NamespaceVersioning` > `URLPathVersioning`. Never mix in the same project.
- Emit `X-API-Supported-Versions` & `X-API-Deprecated-Versions` headers from middleware.
Go Gin
- Store active versions in `map[int]http.Handler`; panic on duplicate registration during boot.
- Provide `/_status` endpoint that lists `supported_versions`, `deprecated_versions`, `sunset_dates`.
Testing
- CI job `contract-diff` blocks merge if breaking change detected via `openapi-diff` comparing `/contracts/v{n}/openapi.yaml`.
- Execute backward-compat tests: new server vs old client stubs; forward-compat: old server vs new client stubs.
- Canary deploy new version behind feature flag; route ≤5 % traffic via API Gateway weighted routing.
Performance
- API Gateway (AWS API Gateway, Kong, Apigee) handles version routing and traffic shaping; offload auth, rate-limit per version.
- Enable caching per version key to avert cache-poisoning (`Cache-Key: version`).
Security
- Security patches are back-ported to all supported versions within 24 h.
- Automatically fail builds if CVEs detected in deprecated but still-supported versions.
- Each OpenAPI doc declares security schemes; lint fails if a new version downgrades TLS requirements.
CI/CD & Tooling
- Branch naming: `version/v{n}.{minor}`; merge to `main` auto-tags repo `v{n}.{minor}.{patch}`.
- GitHub Actions pipeline steps: `validate-contract ➜ unit-test ➜ contract-diff ➜ perf-test ➜ deploy-v{n}`.
- Use Postman v10 and Zuplo sync for live documentation; publish doc at `/docs/v{n}` and show deprecation banner.
Common Pitfalls & Guardrails
- 🔴 Do NOT hide breaking changes behind minor version bumps.
- 🔴 Avoid query-param versioning in public APIs—poor caching & discoverability.
- 🔴 Never silently remove fields; mark as `deprecated: true` in schema and advertise removal date.
- ✅ Automate everything—manual version management guarantees drift.
Directory Blueprint (Node example)
```
├── contracts
│ ├── v1
│ │ └── openapi.yaml
│ └── v2
│ └── openapi.yaml
├── src
│ ├── v1
│ │ ├── controllers
│ │ ├── routes
│ │ └── validators
│ └── v2
│ └── ...
└── tests
├── v1
└── v2
```
Cheat-Sheet Headers
- Accept: `application/vnd.myapp+json;v=2`
- Sunset: `Wed, 11 Jun 2025 00:00:00 GMT`
- Deprecation: `true`
- Link: `</docs/v3>; rel="latest"`
Use these rules to ship reliable, discoverable, and future-proof APIs with zero-downtime version evolution.