Skip to main content
โšก Calmops

API Versioning Deep Dive: Complete Guide

API Versioning Deep Dive: Complete Guide

API versioning is crucial for evolving your API without breaking existing clients. This guide covers different versioning strategies, implementation approaches, and deprecation best practices.

Why API Versioning Matters

  • Allow API changes without breaking clients
  • Support multiple API versions simultaneously
  • Give clients time to migrate
  • Maintain backward compatibility
  • Enable gradual rollouts

Versioning Strategies

1. URL Path Versioning (Most Common)

# Version in URL path
GET /v1/users
GET /v2/users
GET /v3/users

Pros:

  • Easy to understand
  • Simple to test
  • Clear which version is being used

Cons:

  • URL proliferation
  • Harder to maintain single endpoint

2. Header Versioning

# Custom header
GET /users
X-API-Version: 2024-01-01

# Accept header
GET /users
Accept: application/vnd.myapp.v2+json

Pros:

  • Cleaner URLs
  • Multiple versions on same URL

Cons:

  • Less visible
  • Harder to test

3. Query Parameter Versioning

GET /users?version=2

Pros:

  • Simple to implement
  • Easy to test

Cons:

  • Caching issues
  • Can be forgotten

Implementation Examples

URL Path Versioning (Express.js)

const express = require('express');
const app = express();

// V1 routes
app.get('/v1/users', (req, res) => {
  res.json({
    users: db.users.map(u => ({
      id: u.id,
      name: u.name,
      email: u.email
    }))
  });
});

// V2 routes
app.get('/v2/users', (req, res) => {
  res.json({
    users: db.users.map(u => ({
      id: u.id,
      name: u.name,
      email: u.email,
      profile: u.profile,
      preferences: u.preferences
    }))
  });
});

// Latest version redirect
app.get('/users', (req, res) => {
  res.redirect('/v2/users');
});

Version Router Pattern

const v1Routes = require('./routes/v1');
const v2Routes = require('./routes/v2');

const apiRouter = express.Router();

apiRouter.use('/v1', v1Routes);
apiRouter.use('/v2', v2Routes);

app.use('/api', apiRouter);

// Handle unknown versions
app.use('/api/*', (req, res) => {
  res.status(404).json({
    error: {
      code: 'VERSION_NOT_FOUND',
      message: 'API version not supported',
      supportedVersions: ['v1', 'v2']
    }
  });
});

Header Versioning

const versionMiddleware = (req, res, next) => {
  // Check header
  const version = req.headers['x-api-version'] || 'v1';
  
  // Validate version
  if (!['v1', 'v2', 'v3'].includes(version)) {
    return res.status(400).json({
      error: {
        code: 'INVALID_VERSION',
        message: 'Invalid API version',
        supported: ['v1', 'v2', 'v3']
      }
    });
  }
  
  req.apiVersion = version;
  next();
};

app.use('/api', versionMiddleware);

app.get('/api/users', (req, res) => {
  const handlers = {
    v1: getUsersV1,
    v2: getUsersV2,
    v3: getUsersV3
  };
  
  return handlers[req.apiVersion](req, res);
});

Response Format by Version

V1 Response

{
  "users": [
    {
      "id": "123",
      "name": "John Doe",
      "email": "[email protected]"
    }
  ]
}

V2 Response (Breaking Changes)

{
  "data": [
    {
      "id": "123",
      "type": "user",
      "attributes": {
        "name": "John Doe",
        "email": "[email protected]"
      },
      "meta": {
        "created": "2024-01-01T00:00:00Z"
      }
    }
  ],
  "meta": {
    "version": "v2",
    "total": 1
  }
}

Deprecation Strategy

Deprecation Headers

app.get('/v1/users', (req, res) => {
  // Mark as deprecated
  res.set('Deprecation', 'true');
  res.set('Sunset', 'Sat, 01 Jan 2025 00:00:00 GMT');
  res.set('Link', '<https://api.example.com/v2/users>; rel="successor-version"');
  
  res.json({ /* ... */ });
});

Deprecation Response

{
  "users": [],
  "deprecation": {
    "version": "v1",
    "sunsetDate": "2025-01-01",
    "message": "v1 will be discontinued",
    "migrationGuide": "https://api.example.com/docs/v1-to-v2",
    "successorVersion": "v2"
  }
}

Deprecation Schedule

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Deprecation Timeline                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                             โ”‚
โ”‚  v1.0 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>  โ”‚
โ”‚       โ”‚                                                    โ”‚
โ”‚       โ”‚ Announced      Sunset         End of Life          โ”‚
โ”‚       โ”‚    โ”‚              โ”‚                โ”‚               โ”‚
โ”‚       โ–ผ    โ–ผ              โ–ผ                โ–ผ               โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€     โ”‚
โ”‚          โ”‚               โ”‚               โ”‚                 โ”‚
โ”‚          โ”‚   โœ“ Support   โ”‚   โš  Deprecated โ”‚   โœ— Removed  โ”‚
โ”‚          โ”‚               โ”‚               โ”‚                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Version Negotiation

Auto-Deprecation with Warnings

app.use((req, res, next) => {
  const version = req.headers['x-api-version'];
  
  // Warn about old versions
  if (version === 'v1') {
    res.set('Warning', '299 - "v1 is deprecated, please migrate to v2"');
  }
  
  if (!version) {
    // Default to latest version but inform client
    req.apiVersion = 'v2';
    res.set('X-API-Deprecation', 'true');
    res.set('X-API-Sunset-Date', '2025-01-01');
  }
  
  next();
});

Feature Flags by Version

const features = {
  v1: ['basic_users', 'read_only'],
  v2: ['basic_users', 'full_profiles', 'preferences'],
  v3: ['basic_users', 'full_profiles', 'preferences', 'analytics', 'export']
};

app.get('/api/features', (req, res) => {
  const version = req.apiVersion;
  res.json({
    version,
    features: features[version] || features.v3
  });
});

Best Practices

Version Naming

// Good: Semantic versioning
v1.0.0 - First stable release
v1.1.0 - Backward compatible features
v1.2.0 - Backward compatible features
v2.0.0 - Breaking changes
v2.1.0 - Backward compatible

// Good: Date-based versioning
v2024-01-01
v2024-06-01

// Bad: No clear versioning
latest
new
stable

Migration Guide

# Migration Guide: v1 to v2

## Breaking Changes

1. **Response Format**
   - v1: `{ "users": [...] }`
   - v2: `{ "data": [...], "meta": {...} }`

2. **Field Renamed**
   - v1: `username`
   - v2: `login`

3. **Field Removed**
   - Removed: `last_login`
   - Use: `last_seen`

4. **Authentication**
   - v1: API Key in header
   - v2: Bearer token required

## How to Migrate

1. Update endpoint URLs from `/v1/` to `/v2/`
2. Update response parsing for new format
3. Replace `username` with `login`
4. Update authentication headers

Multi-Version Support

Service Implementation

// Separate handlers for each version
const handlers = {
  v1: {
    list: (req, res) => {
      // Simple response
      res.json({ users: db.users.map(u => u.name) });
    },
    get: (req, res) => {
      const user = db.users.find(req.params.id);
      res.json(user);
    }
  },
  v2: {
    list: (req, res) => {
      // Enhanced response with pagination
      const page = parseInt(req.query.page) || 1;
      const limit = parseInt(req.query.limit) || 20;
      res.json({
        data: db.users.map(u => ({ ...u, profile: u.profile })),
        meta: { page, limit, total: db.users.length }
      });
    },
    get: (req, res) => {
      const user = db.users.find(req.params.id);
      res.json({
        data: user,
        included: [user.profile, user.preferences]
      });
    }
  }
};

// Route dispatcher
app.get('/users', (req, res) => {
  const handler = handlers[req.apiVersion]?.list || handlers.v2.list;
  return handler(req, res);
});

External Resources


Comments