Skip to main content
โšก Calmops

API Versioning Strategies: A Practical Guide

Introduction

APIs evolve over time. New features are added, fields are deprecated, and breaking changes are sometimes necessary. API versioning allows you to make these changes without breaking existing clients. This guide covers the main versioning strategies and when to use each.


Versioning Strategies Overview

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                API Versioning Strategies                        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                             โ”‚
โ”‚  1. URL Path Versioning                                     โ”‚
โ”‚     GET /api/v1/users                                      โ”‚
โ”‚     GET /api/v2/users                                      โ”‚
โ”‚     + Most visible, easy to cache                         โ”‚
โ”‚     - URL pollution, harder to maintain                    โ”‚
โ”‚                                                             โ”‚
โ”‚  2. Query Parameter Versioning                             โ”‚
โ”‚     GET /api/users?version=1                              โ”‚
โ”‚     GET /api/users?version=2                              โ”‚
โ”‚     + Clean URLs, optional version                        โ”‚
โ”‚     - Less visible, caching complexity                    โ”‚
โ”‚                                                             โ”‚
โ”‚  3. Header Versioning                                     โ”‚
โ”‚     GET /api/users                                        โ”‚
โ”‚     Accept-Version: v1                                    โ”‚
โ”‚     + Clean URLs, flexible                               โ”‚
โ”‚     - Less discoverable, requires documentation           โ”‚
โ”‚                                                             โ”‚
โ”‚  4. Content Negotiation                                   โ”‚
โ”‚     GET /api/users                                        โ”‚
โ”‚     Accept: application/vnd.api.v1+json                   โ”‚
โ”‚     + Standard HTTP, clean URLs                          โ”‚
โ”‚     - Complex, less intuitive                            โ”‚
โ”‚                                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Strategy 1: URL Path Versioning

Implementation

// Express.js with URL versioning
import express from 'express';
const app = express();

// v1 routes
app.get('/api/v1/users', (req, res) => {
  res.json({
    users: [
      { id: 1, name: 'John', email: '[email protected]' }
    ]
  });
});

// v2 routes
app.get('/api/v2/users', (req, res) => {
  res.json({
    users: [
      { 
        id: 1, 
        name: 'John', 
        email: '[email protected]',
        profile: { bio: 'Hello', avatar: 'url' }
      }
    ]
  });
});

With Router Pattern

// Organize by version
import { Router } from 'express';

const v1Router = Router();
const v2Router = Router();

// v1
v1Router.get('/users', handler.getUsersV1);
v1Router.post('/users', handler.createUserV1);

// v2  
v2Router.get('/users', handler.getUsersV2);
v2Router.post('/users', handler.createUserV2);

// Mount
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

Pros and Cons

url_versioning:
  pros:
    - "Most visible and clear"
    - "Easy to test and debug"
    - "CDN and cache-friendly"
    - "Works with API gateways"
  cons:
    - "URL pollution over time"
    - "Code duplication possible"
    - "Requires redirect for default version"

Strategy 2: Query Parameter Versioning

Implementation

// Query parameter versioning
app.get('/api/users', async (req, res) => {
  const version = parseInt(req.query.version as string) || 1;
  
  if (version === 1) {
    return res.json(await getUsersV1());
  }
  
  if (version === 2) {
    return res.json(await getUsersV2());
  }
  
  res.status(400).json({ error: 'Unsupported version' });
});

// Default version (no query param)
app.get('/api/users', async (req, res) => {
  // Default to latest stable version
  return res.json(await getUsersV2());
});

Express Router Example

// Route handlers per version
const getUsersHandler = (version: number) => {
  return async (req: Request, res: Response) => {
    switch (version) {
      case 1:
        return res.json(await usersService.getUsersV1());
      case 2:
        return res.json(await usersService.getUsersV2());
      default:
        return res.status(400).json({ error: 'Invalid version' });
    }
  };
};

app.get('/api/users', (req, res) => {
  const version = parseInt(req.query.v as string) || getDefaultVersion();
  getUsersHandler(version)(req, res);
});

Strategy 3: Header Versioning

Implementation

// Header-based versioning
app.get('/api/users', async (req, res) => {
  const version = req.headers['x-api-version'] as string || 'v1';
  
  switch (version) {
    case 'v1':
      return res.json(await getUsersV1());
    case 'v2':
      return res.json(await getUsersV2());
    default:
      return res.status(400).json({ error: 'Unsupported version' });
  }
});

Custom Header Middleware

// Middleware for header versioning
const versionMiddleware = (req: Request, res: Response, next: NextFunction) => {
  const version = req.headers['x-api-version'] as string || 'v1';
  req.apiVersion = version;
  next();
};

app.use(versionMiddleware);

// Use in handlers
app.get('/api/users', async (req, res) => {
  const { apiVersion } = req as Request & { apiVersion: string };
  // Handle different versions
});

Strategy 4: Content Negotiation

Implementation

// Content negotiation versioning
app.get('/api/users', async (req, res) => {
  const accept = req.headers.accept;
  
  // Parse custom media type
  const match = accept?.match(/application\/vnd\.api\.v(\d+)\+json/);
  const version = match ? parseInt(match[1]) : 1;
  
  if (version === 1) {
    res.set('Content-Type', 'application/vnd.api.v1+json');
    return res.json(await getUsersV1());
  }
});

Register Custom Media Types

// Register in Express
app.use((req, res, next) => {
  // Add custom formatter
  res.format({
    'application/vnd.api.v1+json': () => {
      res.json(getUsersV1());
    },
    'application/vnd.api.v2+json': () => {
      res.json(getUsersV2());
    },
    'application/json': () => {
      // Default
      res.json(getUsersV2());
    },
    default: () => {
      res.status(406).json({ error: 'Not acceptable' });
    }
  });
  next();
});

Deprecation Strategy

Marking Endpoints as Deprecated

// Add deprecation headers
app.get('/api/v1/users', (req, res) => {
  // Deprecation header (RFC 5988)
  res.set('Deprecation', 'Thu, 01 Jan 2026 00:00:00 GMT');
  
  // Link to new version
  res.set('Link', '<https://api.example.com/v2/users>; rel="alternate"');
  
  // Sunset header (draft)
  res.set('Sunset', 'Thu, 01 Jan 2027 00:00:00 GMT');
  
  // Warning header
  res.set('Warning', '299 - "This endpoint will be removed on January 1, 2027"');
  
  res.json(getUsersV1());
});

Deprecation Schedule

# Version lifecycle
lifecycle:
  experimental:
    - "Newly released"
    - "May change without notice"
    - "Not recommended for production"
    
  stable:
    - "Production-ready"
    - "Fully supported"
    - "Security updates"
    
  deprecated:
    - "Marked for removal"
    - "6-12 months of support"
    - "New code should use newer version"
    
  removed:
    - "No longer available"
    - "Returns 410 Gone"

Best Practices

Version Numbering

# Semantic versioning for APIs
versioning:
  major:
    - "Breaking changes"
    - "Remove fields"
    - "Change field types"
    - "Remove endpoints"
    
  minor:
    - "New features (backward compatible)"
    - "Add new fields"
    - "Add new endpoints"
    
  patch:
    - "Bug fixes"
    - "Documentation updates"

Default Version Strategy

// Always redirect to latest version
app.get('/api/users', (req, res) => {
  // Redirect to v2
  res.redirect(307, '/api/v2/users');
});

// Or use default version header
app.get('/api/users', (req, res) => {
  const version = req.headers['x-api-version'] || 'v2';
  // Handle version
});

Key Takeaways

  • URL Path - Most common, best visibility, easiest to cache
  • Query Parameter - Clean URLs, optional versioning
  • Header - Flexible, less cluttered URLs
  • Content Negotiation - Standard HTTP, more complex setup

External Resources

Comments