API versioning strategies for Node.js backends. Use when implementing versioned APIs.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers API versioning strategies for maintaining backward compatibility.
Use this skill when:
BACKWARD COMPATIBILITY - Existing clients should continue working. New features require new versions only when breaking changes are necessary.
// 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;
// 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;
// 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;
// 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 },
};
},
}));
// 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);
// 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(),
};
}
// 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>;