API Versioning Skill
This skill helps you manage API versions in apps/api/src/v1/ and prepare for future versions.
When to Use This Skill
-
Creating a new API version (v2, v3, etc.)
-
Deprecating old API endpoints
-
Migrating endpoints between versions
-
Planning breaking changes
-
Maintaining backward compatibility
Current API Structure
apps/api/src/ ├── v1/ # Current API version │ ├── routes/ │ │ ├── cars.ts # Car registration endpoints │ │ ├── coe.ts # COE bidding endpoints │ │ ├── pqp.ts # PQP data endpoints │ │ └── health.ts # Health check │ └── index.ts # v1 router assembly └── index.ts # Main Hono app with versioned routes
Versioning Strategy
URL-Based Versioning
The project uses URL path versioning:
Benefits
-
Clear, explicit versioning visible in URLs
-
Easy to cache and monitor per version
-
Clients can migrate at their own pace
-
Multiple versions can coexist
Creating a New API Version
Step 1: Create Version Directory
mkdir -p apps/api/src/v2/routes
Step 2: Copy Existing Routes
Start with current v1 routes as a base:
cp -r apps/api/src/v1/routes/* apps/api/src/v2/routes/
Step 3: Create Version Router
Create apps/api/src/v2/index.ts :
import { Hono } from "hono"; import { carsRouter } from "./routes/cars"; import { coeRouter } from "./routes/coe"; import { pqpRouter } from "./routes/pqp";
const v2 = new Hono();
// Mount routes v2.route("/cars", carsRouter); v2.route("/coe", coeRouter); v2.route("/pqp", pqpRouter);
export default v2;
Step 4: Mount in Main App
Update apps/api/src/index.ts :
import { Hono } from "hono"; import v1 from "./v1"; import v2 from "./v2";
const app = new Hono();
// Mount API versions app.route("/v1", v1); app.route("/v2", v2); // Add new version
// Default to latest stable version app.route("/", v1); // Keep v1 as default or change to v2 when stable
export default app;
Step 5: Implement Breaking Changes
Make necessary changes in v2 routes:
// v1 response format { "success": true, "data": [...], "count": 10 }
// v2 response format (breaking change) { "data": [...], "meta": { "total": 10, "page": 1, "pageSize": 10 } }
Migration Patterns
- Gradual Migration
Keep both versions running:
// v1/routes/cars.ts - deprecated but maintained export const carsRouter = new Hono();
carsRouter.get("/", async (c) => { // Old logic return c.json({ success: true, data: await getCars(), }); });
// v2/routes/cars.ts - new implementation export const carsRouter = new Hono();
carsRouter.get("/", async (c) => { // New logic with pagination const { page = 1, limit = 10 } = c.req.query(); const result = await getCars({ page, limit });
return c.json({ data: result.items, meta: { total: result.total, page, pageSize: limit, }, }); });
- Feature Flag Pattern
Use feature flags to test changes:
import { Hono } from "hono";
export const carsRouter = new Hono();
carsRouter.get("/", async (c) => { const useV2Format = c.req.header("X-API-Version") === "2";
const data = await getCars();
if (useV2Format) { return c.json({ data, meta: { ... } }); }
// v1 format return c.json({ success: true, data }); });
- Deprecation Warnings
Add deprecation headers to v1:
import { Hono } from "hono";
export const carsRouter = new Hono();
// Add deprecation middleware carsRouter.use("*", async (c, next) => { await next(); c.header("X-API-Deprecation", "true"); c.header("X-API-Sunset", "2025-12-31"); c.header("Link", '<https://api.sgcarstrends.com/v2/cars>; rel="successor-version"'); });
carsRouter.get("/", async (c) => { // Existing logic });
Breaking Changes Checklist
When introducing breaking changes, consider:
-
Response structure changes
-
Required parameter additions
-
Authentication method changes
-
URL structure modifications
-
HTTP method changes
-
Header requirement changes
-
Error format modifications
-
Data type changes
Version Documentation
Document versions in OpenAPI/Swagger:
// apps/api/src/v2/openapi.ts import { OpenAPIHono } from "@hono/zod-openapi";
const app = new OpenAPIHono();
app.openapi( { method: "get", path: "/cars", summary: "Get car registrations (v2)", deprecated: false, tags: ["Cars"], responses: { 200: { description: "Success", content: { "application/json": { schema: carResponseSchema, }, }, }, }, }, async (c) => { // Handler } );
Version Sunset Process
- Announce Deprecation
-
Update documentation
-
Add deprecation headers
-
Notify API consumers
-
Set sunset date
- Monitor Usage
Track v1 usage metrics:
import { middleware } from "hono/middleware";
v1.use("*", async (c, next) => { // Log usage for monitoring console.log("v1 API usage:", { path: c.req.path, user: c.get("user")?.id, timestamp: new Date(), });
await next(); });
- Provide Migration Guide
Create migration documentation:
Migrating from v1 to v2
Breaking Changes
Response Format
v1: ```json { "success": true, "data": [...] } ```
v2: ```json { "data": [...], "meta": { ... } } ```
Pagination
v2 includes built-in pagination:
- Query params:
?page=1&limit=10 - Response includes
metawith pagination info
Migration Steps
-
Update base URL from
/v1to/v2 -
Update response parsing to handle new format
-
Add pagination parameters if needed
-
Update error handling for new error format
-
Remove Old Version
After sunset date:
Remove v1 directory
rm -rf apps/api/src/v1
Update main app
Remove v1 mounting from apps/api/src/index.ts
Testing Multiple Versions
Test all active versions:
Test v1
curl https://api.sgcarstrends.com/v1/cars
Test v2
curl https://api.sgcarstrends.com/v2/cars
Run version-specific tests
pnpm -F @sgcarstrends/api test -- src/v1 pnpm -F @sgcarstrends/api test -- src/v2
Deployment Considerations
Zero-Downtime Deployment
-
Deploy v2 alongside v1
-
Test v2 in production
-
Gradually route traffic to v2
-
Monitor error rates
-
Rollback if issues occur
Environment Variables
Version-specific config:
v1 settings
V1_RATE_LIMIT=100 V1_CACHE_TTL=300
v2 settings
V2_RATE_LIMIT=200 V2_CACHE_TTL=600
Common Scenarios
Scenario 1: Add Required Parameter
v1: Optional parameter
carsRouter.get("/", async (c) => { const make = c.req.query("make"); // optional return c.json(await getCars({ make })); });
v2: Required parameter (breaking change)
carsRouter.get("/", async (c) => { const make = c.req.query("make"); if (!make) { return c.json({ error: "make parameter required" }, 400); } return c.json(await getCars({ make })); });
Scenario 2: Change Data Format
v1: Flat structure
{ id: 1, make: "Toyota", model: "Camry" }
v2: Nested structure (breaking change)
{ id: 1, vehicle: { make: "Toyota", model: "Camry" } }
Scenario 3: Rename Endpoint
v1: /cars/list
v2: /cars (breaking change - URL structure)
Solution: Redirect in v1
v1.get("/cars/list", async (c) => { c.header("X-API-Deprecated", "true"); c.header("Location", "/v2/cars"); return c.redirect("/v2/cars", 301); });
References
-
Hono documentation: Use Context7 for latest docs
-
Related files:
-
apps/api/src/v1/
-
Current API version
-
apps/api/src/index.ts
-
Main app with version mounting
-
apps/api/CLAUDE.md
-
API service documentation
Best Practices
-
Semantic Versioning: Use v1, v2, v3 (not v1.1, v1.2)
-
Backward Compatibility: Maintain old versions during migration period
-
Documentation: Document all breaking changes clearly
-
Communication: Announce deprecations well in advance
-
Monitoring: Track usage of deprecated endpoints
-
Testing: Maintain tests for all active versions
-
Graceful Sunset: Provide sufficient migration time (6-12 months)
-
Error Messages: Help users migrate with clear error messages