Home/Blog/Cybersecurity/API Versioning Strategies: URL, Header, and Query Parameter Approaches
Cybersecurity

API Versioning Strategies: URL, Header, and Query Parameter Approaches

Choose the right API versioning strategy for your use case. Covers URL path, header, and query parameter versioning with deprecation and migration best practices.

By Inventive HQ Team
API Versioning Strategies: URL, Header, and Query Parameter Approaches

API versioning allows you to evolve your API while maintaining backwards compatibility. This guide covers versioning strategies, implementation patterns, and deprecation best practices.

Versioning Strategy Comparison

┌─────────────────────────────────────────────────────────────────────────────┐
│                      API VERSIONING STRATEGIES                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  URL PATH VERSIONING                                                        │
│  ────────────────────                                                       │
│  GET /api/v1/users                                                          │
│  GET /api/v2/users                                                          │
│                                                                              │
│  ✓ Easy to implement and understand                                         │
│  ✓ Visible in logs, easy to debug                                          │
│  ✓ Browser-testable, cacheable                                             │
│  ✗ "Impure" REST (version isn't a resource)                                │
│  ✗ URL changes when version changes                                        │
│                                                                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  HEADER VERSIONING                                                          │
│  ─────────────────                                                          │
│  GET /api/users                                                             │
│  Accept: application/vnd.myapi.v2+json                                      │
│  - OR -                                                                      │
│  Api-Version: 2                                                             │
│                                                                              │
│  ✓ Clean URLs                                                               │
│  ✓ "Pure" REST (content negotiation)                                       │
│  ✗ Harder to test (need headers)                                           │
│  ✗ Caching requires Vary header                                            │
│  ✗ Less visible in logs                                                    │
│                                                                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  QUERY PARAMETER VERSIONING                                                 │
│  ───────────────────────────                                                │
│  GET /api/users?version=2                                                   │
│                                                                              │
│  ✓ Simple to implement                                                      │
│  ✓ Easy to test                                                            │
│  ✗ Can conflict with other params                                          │
│  ✗ Often ignored in caching                                                │
│  ✗ Looks like optional parameter                                           │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Breaking vs Non-Breaking Changes

Change TypeBreaking?Action
Remove field from response✅ YesNew version
Remove endpoint✅ YesNew version
Rename field✅ YesNew version
Change field type (string→number)✅ YesNew version
Change authentication method✅ YesNew version
Change error format✅ YesNew version
Add new optional field❌ NoCurrent version
Add new endpoint❌ NoCurrent version
Add new optional parameter❌ NoCurrent version
Bug fix (same behavior)❌ NoCurrent version
Performance improvement❌ NoCurrent version

URL Path Versioning

Express.js Implementation

import express from 'express';

const app = express();

// Version 1 routes
const v1Router = express.Router();
v1Router.get('/users', (req, res) => {
  res.json({
    users: [
      { id: 1, name: 'John', email: '[email protected]' }
    ]
  });
});

// Version 2 routes (different response structure)
const v2Router = express.Router();
v2Router.get('/users', (req, res) => {
  res.json({
    data: [
      {
        id: 1,
        attributes: {
          name: 'John',
          email: '[email protected]'
        }
      }
    ],
    meta: { total: 1 }
  });
});

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

// Redirect unversioned to latest stable
app.use('/api/users', (req, res) => {
  res.redirect(308, `/api/v2${req.path}`);
});

Shared Code Between Versions

// services/users.service.ts - Shared business logic
export class UsersService {
  async getUsers(filters: UserFilters): Promise<User[]> {
    return await prisma.user.findMany({ where: filters });
  }
}

// v1/users.controller.ts
import { UsersService } from '../services/users.service';

const usersService = new UsersService();

export async function getUsers(req: Request, res: Response) {
  const users = await usersService.getUsers(req.query);

  // V1 response format
  res.json({ users });
}

// v2/users.controller.ts
import { UsersService } from '../services/users.service';

const usersService = new UsersService();

export async function getUsers(req: Request, res: Response) {
  const users = await usersService.getUsers(req.query);

  // V2 response format (JSON:API style)
  res.json({
    data: users.map(u => ({
      id: u.id,
      type: 'user',
      attributes: { name: u.name, email: u.email }
    })),
    meta: { total: users.length }
  });
}

Header Versioning

Custom Header Implementation

// Middleware to extract version from header
function versionMiddleware(req: Request, res: Response, next: NextFunction) {
  const version = req.headers['api-version'] || req.headers['x-api-version'];

  if (!version) {
    // Default to latest stable
    req.apiVersion = 2;
  } else {
    const parsed = parseInt(version as string, 10);
    if (isNaN(parsed) || parsed < 1 || parsed > 2) {
      return res.status(400).json({
        error: 'Invalid API version',
        supported: [1, 2],
      });
    }
    req.apiVersion = parsed;
  }

  // Add version to response
  res.setHeader('Api-Version', req.apiVersion);
  next();
}

// Route handler using version
app.get('/api/users', versionMiddleware, async (req, res) => {
  const users = await usersService.getUsers(req.query);

  if (req.apiVersion === 1) {
    return res.json({ users });
  }

  // Version 2
  return res.json({
    data: users.map(formatUserV2),
    meta: { total: users.length }
  });
});

Accept Header (Content Negotiation)

// Using media type versioning
// Accept: application/vnd.myapi.v2+json

function parseAcceptVersion(accept: string | undefined): number {
  if (!accept) return 2; // Default

  const match = accept.match(/application\/vnd\.myapi\.v(\d+)\+json/);
  return match ? parseInt(match[1], 10) : 2;
}

app.get('/api/users', (req, res) => {
  const version = parseAcceptVersion(req.headers.accept);

  // Set appropriate content type
  res.setHeader(
    'Content-Type',
    `application/vnd.myapi.v${version}+json`
  );

  // Version-specific response
  // ...
});

API Gateway Routing

Kong Configuration

# kong.yml
services:
  - name: api-v1
    url: http://api-v1-service:8080
    routes:
      - name: api-v1-route
        paths:
          - /api/v1

  - name: api-v2
    url: http://api-v2-service:8080
    routes:
      - name: api-v2-route
        paths:
          - /api/v2

  # Header-based routing
  - name: api-v1-header
    url: http://api-v1-service:8080
    routes:
      - name: api-v1-header-route
        paths:
          - /api
        headers:
          api-version:
            - "1"

  - name: api-v2-header
    url: http://api-v2-service:8080
    routes:
      - name: api-v2-header-route
        paths:
          - /api
        headers:
          api-version:
            - "2"

AWS API Gateway

# SAM template
Resources:
  ApiV1:
    Type: AWS::Serverless::Api
    Properties:
      StageName: v1
      DefinitionUri: ./openapi-v1.yaml

  ApiV2:
    Type: AWS::Serverless::Api
    Properties:
      StageName: v2
      DefinitionUri: ./openapi-v2.yaml

  # Custom domain with base path mappings
  ApiDomainMapping:
    Type: AWS::ApiGatewayV2::ApiMapping
    Properties:
      DomainName: api.example.com
      ApiId: !Ref ApiV2
      Stage: v2
      ApiMappingKey: v2

  ApiDomainMappingV1:
    Type: AWS::ApiGatewayV2::ApiMapping
    Properties:
      DomainName: api.example.com
      ApiId: !Ref ApiV1
      Stage: v1
      ApiMappingKey: v1

Deprecation Strategy

Deprecation Headers

// Middleware to add deprecation headers
function deprecationMiddleware(
  deprecationDate: string,
  sunsetDate: string,
  link: string
) {
  return (req: Request, res: Response, next: NextFunction) => {
    // RFC 8594 Deprecation header
    res.setHeader('Deprecation', deprecationDate);

    // RFC 8594 Sunset header
    res.setHeader('Sunset', sunsetDate);

    // Link to migration guide
    res.setHeader('Link', `<${link}>; rel="deprecation"`);

    // Optional warning header
    res.setHeader(
      'Warning',
      '299 - "This API version is deprecated. Please migrate to v2."'
    );

    next();
  };
}

// Apply to v1 routes
app.use(
  '/api/v1',
  deprecationMiddleware(
    'Sun, 01 Jan 2025 00:00:00 GMT',  // Deprecated since
    'Mon, 01 Jul 2025 00:00:00 GMT',  // Will be removed
    'https://docs.example.com/migration-guide'
  ),
  v1Router
);

Version Lifecycle

┌─────────────────────────────────────────────────────────────────────────────┐
│                      API VERSION LIFECYCLE                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Timeline    Status          Actions                                        │
│  ────────────────────────────────────────────────────────────────────────── │
│                                                                              │
│  T+0         CURRENT         - Active development                           │
│              (v2)            - All new features added here                  │
│                              - Full support                                 │
│                                                                              │
│  T+0         DEPRECATED      - Security fixes only                          │
│              (v1)            - Deprecation headers added                    │
│                              - Migration guide published                    │
│                              - Email notification sent                      │
│                                                                              │
│  T+6mo       DEPRECATED      - Monitor usage metrics                        │
│              (v1)            - Contact high-volume users                    │
│                              - Reminder emails                              │
│                                                                              │
│  T+12mo      SUNSET WARNING  - Final notice emails                          │
│              (v1)            - Dashboard warnings                           │
│                              - Rate limits may be reduced                   │
│                                                                              │
│  T+18mo      SUNSET          - Version removed                              │
│              (v1)            - Returns 410 Gone                             │
│                              - Redirect to migration docs                   │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Sunset Response

// After sunset date, return 410 Gone
app.use('/api/v1', (req, res) => {
  const sunsetDate = new Date('2025-07-01');

  if (new Date() > sunsetDate) {
    return res.status(410).json({
      error: 'VERSION_SUNSET',
      message: 'API v1 has been sunset. Please migrate to v2.',
      migrationGuide: 'https://docs.example.com/migration-v1-v2',
      currentVersion: 'https://api.example.com/v2',
    });
  }

  // Still active, continue with deprecation warnings
  next();
});

Version Migration Guide Template

# Migrating from API v1 to v2

## Overview
API v2 introduces a new response format following JSON:API specification,
improved error handling, and new endpoints for batch operations.

## Timeline
- **Deprecation Date**: January 1, 2025
- **Sunset Date**: July 1, 2025
- **Support Email**: [email protected]

## Breaking Changes

### Response Format
**v1:**
```json
{
  "users": [{"id": 1, "name": "John"}]
}

v2:

{
  "data": [{"id": 1, "type": "user", "attributes": {"name": "John"}}],
  "meta": {"total": 1}
}

Authentication

  • v1: API key in query parameter (?api_key=xxx)
  • v2: API key in header (Authorization: Bearer xxx)

Removed Endpoints

  • GET /users/search → Use GET /users?filter[name]=xxx
  • POST /users/bulk → Use POST /users with array body

Migration Steps

  1. Update authentication to use header
  2. Update response parsing for new format
  3. Update any deprecated endpoint calls
  4. Test thoroughly in staging
  5. Deploy and monitor errors

Code Examples

[SDK examples for major languages]

Support

Contact [email protected] for migration assistance.


## Security Considerations

```typescript
// Ensure auth/authz applies to ALL versions
function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization;

  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Validate token (same logic for all versions)
  try {
    req.user = verifyToken(token);
    next();
  } catch (e) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Apply BEFORE version routing
app.use('/api', authMiddleware);
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// Rate limiting across versions
const rateLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  keyGenerator: (req) => {
    // Same key regardless of version
    return req.user?.id || req.ip;
  },
});

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

Monitoring Versions

// Track version usage for deprecation decisions
app.use('/api/:version', (req, res, next) => {
  const version = req.params.version;
  const clientId = req.user?.id || 'anonymous';

  // Log to analytics
  analytics.track({
    event: 'api_request',
    properties: {
      version,
      clientId,
      endpoint: req.path,
      method: req.method,
    },
  });

  // Prometheus metric
  apiRequestsTotal.inc({
    version,
    endpoint: req.path,
    method: req.method,
  });

  next();
});

Best Practices

  1. Choose one strategy - Consistency across your API
  2. Version early - Start with /v1 even if no v2 planned
  3. Global versioning - All endpoints at same version
  4. Semantic meaning - Major version for breaking changes
  5. Long deprecation periods - 12-24 months minimum
  6. Clear documentation - Migration guides for each version
  7. Monitor usage - Data-driven sunset decisions
  8. Security parity - Same security controls across versions
  9. Deprecation headers - Machine-readable warnings
  10. No silent changes - Even non-breaking changes need docs

Next Steps

Need Expert Cybersecurity Guidance?

Our team of security experts is ready to help protect your business from evolving threats.