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

Frequently Asked Questions

Find answers to common questions

API versioning allows you to make breaking changes without disrupting existing clients. Without versioning, changes to request/response formats, removed fields, or changed behavior would break all clients simultaneously. Versioning lets you evolve your API while maintaining backwards compatibility for clients that haven't updated, then deprecate old versions on a schedule.

URL path versioning (/v1/users) is the most common and easiest to implement, cache, and debug. Header versioning (Accept: application/vnd.api+json;version=1) is more RESTful but harder to test. Query parameter versioning (?version=1) is simple but can conflict with other params. Choose URL path for most cases, header for strict REST compliance, query param for simple internal APIs.

Breaking changes require a new version: removing fields/endpoints, renaming fields, changing field types, changing authentication, altering error formats, or changing endpoint URLs. Non-breaking changes can go into the current version: adding new optional fields, adding new endpoints, adding new optional parameters, or bug fixes that don't change behavior. Always add, never remove or modify.

Industry standard is 12-24 months deprecation notice before sunsetting a version. Announce deprecation with timeline, add Deprecation and Sunset headers to responses, notify developers via email/dashboard, monitor usage to identify stragglers, and provide migration guides. Enterprise APIs may need longer support (2-3 years). Consider usage-based decisions—sunset when traffic drops below threshold.

Version the entire API (global versioning) for consistency—all endpoints at /v1/ or /v2/. Per-endpoint versioning (/users/v2) creates confusion and maintenance burden. If only some endpoints need breaking changes, keep them in the new version and have unchanged endpoints respond identically in both versions. Clients should know "I'm on v2" not "I'm on users v2 but orders v1".

Semantic versioning (SemVer) uses MAJOR.MINOR.PATCH format. For APIs: MAJOR for breaking changes requiring client updates, MINOR for backwards-compatible new features, PATCH for backwards-compatible bug fixes. Most public APIs only version by MAJOR (v1, v2) since clients primarily care about breaking changes. Use full SemVer internally or in SDK versioning.

Include version in the URL path: /api/v1/users. In Express: app.use('/api/v1', v1Router); app.use('/api/v2', v2Router). In API gateway, route /v1/* to one backend and /v2/* to another. Each version can be a separate deployment or code branch. Use middleware to extract version and set context for shared code paths.

Use custom headers (Api-Version: 2) or Accept header with vendor MIME type (Accept: application/vnd.myapi.v2+json). Extract in middleware, default to latest stable if missing. Headers are cleaner URLs but harder to test (can't paste URL in browser), harder to cache (Vary header needed), and less visible in logs. Consider hybrid—support both header and URL.

Options: default to latest stable version (can break clients on updates), default to oldest supported version (conservative), or require version (return 400/404). Best practice: default to latest stable for new APIs, keep defaulting to v1 forever for established APIs with many clients. Document the default behavior clearly. Never silently change the default version.

Older versions may have unfixed security vulnerabilities—establish security support windows and force migration for critical fixes. Don't let version negotiation bypass authentication/authorization checks. Ensure rate limits apply across versions (not per-version). Deprecate versions with known vulnerabilities aggressively. Log which versions clients use for security audit.

Don't wait for a breach to act

Get a free security assessment. Our experts will identify your vulnerabilities and create a protection plan tailored to your business.