← Back to list

api-versioning
by IvanTorresEdge
⭐ 0🍴 1📅 Jan 13, 2026
SKILL.md
name: api-versioning description: API versioning strategies for Node.js backends. Use when implementing versioned APIs.
API Versioning Skill
This skill covers API versioning strategies for maintaining backward compatibility.
When to Use
Use this skill when:
- Building public APIs
- Planning for API evolution
- Supporting multiple API versions
- Deprecating old endpoints
Core Principle
BACKWARD COMPATIBILITY - Existing clients should continue working. New features require new versions only when breaking changes are necessary.
Versioning Strategies
1. URL Path Versioning (Recommended)
// src/routes/index.ts
import { FastifyPluginAsync } from 'fastify';
const routes: FastifyPluginAsync = async (fastify) => {
// Version 1
await fastify.register(import('./v1'), { prefix: '/api/v1' });
// Version 2
await fastify.register(import('./v2'), { prefix: '/api/v2' });
};
export default routes;
// src/routes/v1/users.ts
import { FastifyPluginAsync } from 'fastify';
const usersV1: FastifyPluginAsync = async (fastify) => {
fastify.get('/', async () => {
// V1 response format
return fastify.db.user.findMany({
select: { id: true, name: true, email: true },
});
});
};
export default usersV1;
// src/routes/v2/users.ts
import { FastifyPluginAsync } from 'fastify';
const usersV2: FastifyPluginAsync = async (fastify) => {
fastify.get('/', async () => {
// V2 response format with pagination
return {
data: await fastify.db.user.findMany(),
meta: {
total: await fastify.db.user.count(),
page: 1,
perPage: 20,
},
};
});
};
export default usersV2;
2. Header-Based Versioning
// src/plugins/api-version.ts
import { FastifyPluginAsync, FastifyRequest } from 'fastify';
import fp from 'fastify-plugin';
declare module 'fastify' {
interface FastifyRequest {
apiVersion: string;
}
}
const apiVersionPlugin: FastifyPluginAsync = async (fastify) => {
fastify.decorateRequest('apiVersion', '');
fastify.addHook('onRequest', async (request) => {
const version = request.headers['api-version'] as string | undefined;
request.apiVersion = version ?? '1';
});
};
export default fp(apiVersionPlugin);
// src/routes/users.ts
import { FastifyPluginAsync } from 'fastify';
const users: FastifyPluginAsync = async (fastify) => {
fastify.get('/', async (request) => {
const users = await fastify.db.user.findMany();
// Response based on version
if (request.apiVersion === '2') {
return {
data: users,
meta: { total: users.length },
};
}
// V1 default response
return users;
});
};
export default users;
3. Query Parameter Versioning
// src/routes/users.ts
import { FastifyPluginAsync } from 'fastify';
import { z } from 'zod';
const QuerySchema = z.object({
version: z.enum(['1', '2']).default('1'),
});
const users: FastifyPluginAsync = async (fastify) => {
fastify.get<{ Querystring: z.infer<typeof QuerySchema> }>('/', async (request) => {
const { version } = request.query;
const users = await fastify.db.user.findMany();
if (version === '2') {
return { data: users, meta: { total: users.length } };
}
return users;
});
};
export default users;
Version Router Pattern
// src/lib/version-router.ts
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
type VersionHandler<T> = (
request: FastifyRequest,
reply: FastifyReply
) => Promise<T>;
interface VersionedHandlers<T> {
v1: VersionHandler<T>;
v2?: VersionHandler<T>;
v3?: VersionHandler<T>;
}
export function createVersionedHandler<T>(
handlers: VersionedHandlers<T>
): VersionHandler<T> {
return async (request, reply) => {
const version = request.apiVersion as keyof VersionedHandlers<T>;
const handler = handlers[version] ?? handlers.v1;
return handler(request, reply);
};
}
// Usage
import { createVersionedHandler } from '../lib/version-router';
fastify.get('/users', createVersionedHandler({
v1: async (request) => {
return fastify.db.user.findMany();
},
v2: async (request) => {
return {
data: await fastify.db.user.findMany(),
meta: { version: 2 },
};
},
}));
Deprecation Handling
// src/plugins/deprecation.ts
import { FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
interface DeprecatedRoute {
path: string;
method: string;
sunsetDate: string;
alternative?: string;
}
const deprecatedRoutes: DeprecatedRoute[] = [
{
path: '/api/v1/users',
method: 'GET',
sunsetDate: '2025-06-01',
alternative: '/api/v2/users',
},
];
const deprecationPlugin: FastifyPluginAsync = async (fastify) => {
fastify.addHook('onSend', async (request, reply) => {
const deprecated = deprecatedRoutes.find(
(r) => r.path === request.url && r.method === request.method
);
if (deprecated) {
reply.header('Deprecation', `date="${deprecated.sunsetDate}"`);
reply.header('Sunset', deprecated.sunsetDate);
if (deprecated.alternative) {
reply.header('Link', `<${deprecated.alternative}>; rel="successor-version"`);
}
}
});
};
export default fp(deprecationPlugin);
Response Transformers
// src/transformers/user.ts
import { User } from '@prisma/client';
interface UserV1Response {
id: string;
name: string;
email: string;
}
interface UserV2Response {
id: string;
fullName: string;
emailAddress: string;
createdAt: string;
updatedAt: string;
}
export function toUserV1(user: User): UserV1Response {
return {
id: user.id,
name: user.name,
email: user.email,
};
}
export function toUserV2(user: User): UserV2Response {
return {
id: user.id,
fullName: user.name,
emailAddress: user.email,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
};
}
Versioned Schemas
// src/schemas/user.ts
import { z } from 'zod';
// V1 Schema
export const UserV1Schema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
});
// V2 Schema - Added fields
export const UserV2Schema = z.object({
id: z.string(),
fullName: z.string(),
emailAddress: z.string(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
profile: z.object({
avatar: z.string().nullable(),
bio: z.string().nullable(),
}).optional(),
});
export type UserV1 = z.infer<typeof UserV1Schema>;
export type UserV2 = z.infer<typeof UserV2Schema>;
Breaking vs Non-Breaking Changes
Non-Breaking (Add to existing version)
- Adding new endpoints
- Adding optional fields to responses
- Adding optional query parameters
- Loosening validation rules
Breaking (Requires new version)
- Removing fields from responses
- Renaming fields
- Changing field types
- Tightening validation rules
- Removing endpoints
- Changing endpoint paths
Best Practices
- Default version - Always have a default version
- Document changes - Maintain changelog per version
- Deprecation notices - Use headers for deprecation warnings
- Sunset dates - Communicate end-of-life dates
- Support window - Support at least 2 versions
- Breaking changes - Only in major version bumps
Notes
- URL versioning is most explicit and cacheable
- Header versioning keeps URLs clean
- Support minimum 2 versions simultaneously
- Document version differences in API docs
Score
Total Score
60/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon