| name | api-version |
| description | Manage API versioning strategy for Hono routes in apps/api/src/v1. Use when creating new API versions or migrating endpoints between versions. |
| allowed-tools | Read, Edit, Write, Grep, Glob |
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:
https://api.sgcarstrends.com/v1/carshttps://api.sgcarstrends.com/v1/coe- Future:
https://api.sgcarstrends.com/v2/cars
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
1. 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,
},
});
});
2. 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 });
});
3. 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
1. Announce Deprecation
- Update documentation
- Add deprecation headers
- Notify API consumers
- Set sunset date
2. 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();
});
3. 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 `meta` with pagination info
## Migration Steps
1. Update base URL from `/v1` to `/v2`
2. Update response parsing to handle new format
3. Add pagination parameters if needed
4. Update error handling for new error format
4. 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 versionapps/api/src/index.ts- Main app with version mountingapps/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