Claude Code Plugins

Community-maintained marketplace

Feedback

API versioning strategies for Node.js backends. Use when implementing versioned APIs.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

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

  1. Default version - Always have a default version
  2. Document changes - Maintain changelog per version
  3. Deprecation notices - Use headers for deprecation warnings
  4. Sunset dates - Communicate end-of-life dates
  5. Support window - Support at least 2 versions
  6. 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