REST API Versioning Best Practices: Complete Guide with Examples

Comprehensive guide to REST API versioning strategies with real-world examples, best practices, and implementation code using Node.js and Express.js.

Table of Contents

Why API Versioning Matters for REST APIs

REST API versioning ensures that existing clients continue functioning when you introduce changes. Without proper versioning, updates can break client applications, leading to service disruptions and frustrated developers. Good versioning practices allow you to:

REST API Versioning Best Practices: Three Proven Strategies

Let’s explore the most effective API versioning best practices with practical implementation examples using Node.js and Express.js.

URL versioning (also called path versioning) embeds the version number directly in the API endpoint URL. This is the most widely adopted approach among REST APIs due to its simplicity and transparency.

API URL Versioning Best Practices:

Pros:

Cons:

URL Versioning Example Implementation:

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

// REST API Version 1: Basic user data
app.get('/api/v1/users', (req, res) => {
  res.json({
    version: "1.0",
    users: [
      { id: 1, name: 'Alice', status: 'active' },
      { id: 2, name: 'Bob', status: 'active' }
    ]
  });
});

// REST API Version 1: Single user endpoint
app.get('/api/v1/users/:id', (req, res) => {
  const userId = parseInt(req.params.id);
  const user = { id: userId, name: userId === 1 ? 'Alice' : 'Bob', status: 'active' };
  res.json(user);
});

// REST API Version 2: Enhanced user data with email and metadata
app.get('/api/v2/users', (req, res) => {
  res.json({
    version: "2.0",
    users: [
      { 
        id: 1, 
        name: 'Alice', 
        email: 'alice@example.com',
        status: 'active',
        created_at: '2024-01-15T10:30:00Z',
        last_login: '2024-12-01T14:22:00Z'
      },
      { 
        id: 2, 
        name: 'Bob', 
        email: 'bob@example.com',
        status: 'active',
        created_at: '2024-02-20T09:15:00Z',
        last_login: '2024-11-28T16:45:00Z'
      }
    ],
    pagination: {
      page: 1,
      per_page: 10,
      total: 2
    }
  });
});

// REST API Version 2: Enhanced single user with additional fields
app.get('/api/v2/users/:id', (req, res) => {
  const userId = parseInt(req.params.id);
  const users = {
    1: { 
      id: 1, 
      name: 'Alice', 
      email: 'alice@example.com',
      status: 'active',
      created_at: '2024-01-15T10:30:00Z',
      last_login: '2024-12-01T14:22:00Z'
    },
    2: { 
      id: 2, 
      name: 'Bob', 
      email: 'bob@example.com',
      status: 'active',
      created_at: '2024-02-20T09:15:00Z',
      last_login: '2024-11-28T16:45:00Z'
    }
  };
  
  const user = users[userId];
  if (user) {
    res.json(user);
  } else {
    res.status(404).json({ error: 'User not found' });
  }
});

app.listen(3000, () => console.log('REST API server running on port 3000'));

Client Examples for URL Versioning:

Version 1 Request:

curl http://localhost:3000/api/v1/users

Version 1 Response:

{
  "version": "1.0",
  "users": [
    {"id": 1, "name": "Alice", "status": "active"},
    {"id": 2, "name": "Bob", "status": "active"}
  ]
}

Version 2 Request:

curl http://localhost:3000/api/v2/users

Version 2 Response:

{
  "version": "2.0",
  "users": [
    {
      "id": 1,
      "name": "Alice",
      "email": "alice@example.com",
      "status": "active",
      "created_at": "2024-01-15T10:30:00Z",
      "last_login": "2024-12-01T14:22:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 10,
    "total": 2
  }
}

Real-world examples: Popular APIs using this approach include Stripe (/v1/charges), Twitter (/1.1/statuses), and GitHub (/v3/repos).

2. Header-Based REST API Versioning

Header versioning keeps URLs clean by specifying the API version through HTTP headers. This approach follows REST principles more closely by using HTTP’s content negotiation capabilities.

Header Versioning Best Practices:

Pros:

Cons:

Header Versioning Example Implementation:

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

// Middleware for version detection and validation
app.use((req, res, next) => {
  const apiVersion = req.headers['api-version'] || req.headers['x-api-version'];
  const acceptHeader = req.headers['accept'];
  
  // Check custom header first
  if (apiVersion) {
    req.apiVersion = apiVersion;
  }
  // Check Accept header for media type versioning
  else if (acceptHeader && acceptHeader.includes('application/vnd.myapi.v')) {
    const versionMatch = acceptHeader.match(/application\/vnd\.myapi\.v(\d+)/);
    req.apiVersion = versionMatch ? versionMatch[1] : '1';
  }
  // Default to version 1
  else {
    req.apiVersion = '1';
  }
  
  // Validate version
  if (!['1', '2'].includes(req.apiVersion)) {
    return res.status(400).json({ 
      error: 'Unsupported API version',
      supported_versions: ['1', '2']
    });
  }
  
  next();
});

// Single endpoint that responds based on version
app.get('/api/users', (req, res) => {
  const version = req.apiVersion;
  
  if (version === '1') {
    res.json({
      version: "1.0",
      users: [
        { id: 1, name: 'Alice', status: 'active' },
        { id: 2, name: 'Bob', status: 'active' }
      ]
    });
  } else if (version === '2') {
    res.json({
      version: "2.0",
      users: [
        { 
          id: 1, 
          name: 'Alice', 
          email: 'alice@example.com',
          status: 'active',
          profile: {
            department: 'Engineering',
            role: 'Senior Developer'
          }
        },
        { 
          id: 2, 
          name: 'Bob', 
          email: 'bob@example.com',
          status: 'active',
          profile: {
            department: 'Marketing',
            role: 'Content Manager'
          }
        }
      ],
      pagination: {
        page: 1,
        per_page: 10,
        total: 2
      }
    });
  }
});

app.listen(3000, () => console.log('Header-versioned REST API running on port 3000'));

Client Examples for Header Versioning:

Using Custom API-Version Header (Version 1):

curl http://localhost:3000/api/users -H "API-Version: 1"

Using Accept Header (Version 2):

curl http://localhost:3000/api/users -H "Accept: application/vnd.myapi.v2+json"

Default Version (no header):

curl http://localhost:3000/api/users

Real-world example: This approach is used by APIs like Microsoft Graph API and some GitHub API endpoints.

3. Hostname-Based REST API Versioning

Hostname versioning uses different subdomains or entirely different domains for each API version. This provides complete isolation between versions and is ideal for major architectural changes.

Hostname Versioning Best Practices:

Pros:

Cons:

Hostname Versioning Example Implementation:

Version 1 Server (v1.localhost:3000):

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

app.get('/api/users', (req, res) => {
  res.json({
    api_version: "1.0",
    hostname: "v1.api.example.com",
    users: [
      { id: 1, name: 'Alice', status: 'active' },
      { id: 2, name: 'Bob', status: 'active' }
    ]
  });
});

app.listen(3000, () => console.log('Version 1 API running on port 3000'));

Version 2 Server (v2.localhost:3001):

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

app.get('/api/users', (req, res) => {
  res.json({
    api_version: "2.0",
    hostname: "v2.api.example.com",
    users: [
      { 
        id: 1, 
        name: 'Alice', 
        email: 'alice@example.com',
        status: 'active',
        metadata: {
          created_at: '2024-01-15T10:30:00Z',
          updated_at: '2024-12-01T14:22:00Z',
          permissions: ['read', 'write']
        }
      },
      { 
        id: 2, 
        name: 'Bob', 
        email: 'bob@example.com',
        status: 'active',
        metadata: {
          created_at: '2024-02-20T09:15:00Z',
          updated_at: '2024-11-28T16:45:00Z',
          permissions: ['read']
        }
      }
    ],
    features: {
      pagination: true,
      filtering: true,
      sorting: true
    }
  });
});

app.listen(3001, () => console.log('Version 2 API running on port 3001'));

Client Examples for Hostname Versioning:

Version 1 Request:

curl http://v1.localhost:3000/api/users

Version 2 Request:

curl http://v2.localhost:3001/api/users

Advanced REST API Versioning Best Practices

1. Semantic Versioning for APIs

Follow semantic versioning principles for your REST API versioning:

2. Content Negotiation Example

Combine multiple versioning strategies for flexibility:

app.get('/api/users', (req, res) => {
  const urlVersion = req.params.version;
  const headerVersion = req.headers['api-version'];
  const acceptVersion = req.headers['accept']?.match(/v(\d+)/)?.[1];
  
  const version = urlVersion || headerVersion || acceptVersion || '1';
  
  // Version-specific logic here
  handleVersionedResponse(res, version);
});

3. API Deprecation Strategy

Implement proper deprecation warnings in your REST API versioning:

app.use('/api/v1/*', (req, res, next) => {
  res.set('Warning', '299 - "API v1 is deprecated. Please migrate to v2 by 2025-12-31"');
  res.set('Sunset', 'Wed, 31 Dec 2025 23:59:59 GMT');
  next();
});

4. Version Detection Middleware

Create reusable middleware for version handling:

const versionMiddleware = (supportedVersions = ['1', '2']) => {
  return (req, res, next) => {
    const version = req.headers['api-version'] || 
                   req.query.version || 
                   req.params.version || '1';
    
    if (!supportedVersions.includes(version)) {
      return res.status(400).json({
        error: 'Unsupported API version',
        supported_versions: supportedVersions,
        requested_version: version
      });
    }
    
    req.apiVersion = version;
    res.set('API-Version', version);
    next();
  };
};

API Versioning Examples: When to Use Each Strategy

Choose URL Versioning When:

Real-world example: Stripe API uses URL versioning (https://api.stripe.com/v1/charges)

Choose Header Versioning When:

Real-world example: Microsoft Graph API uses header versioning with Accept: application/json;odata.metadata=minimal;odata.version=4.0

Choose Hostname Versioning When:

Real-world example: AWS services use hostname versioning for different API generations

Common REST API Versioning Mistakes to Avoid

  1. Inconsistent versioning schemes: Don’t mix URL and header versioning randomly
  2. No default version: Always provide a sensible default when version isn’t specified
  3. Breaking changes in minor versions: Reserve breaking changes for major version bumps
  4. Poor documentation: Each version needs clear documentation and migration guides
  5. No deprecation strategy: Plan version sunset timelines from the beginning

REST API Versioning Implementation Checklist

Essential Steps for API Versioning Best Practices:

Testing Your API Versioning Strategy

Here’s a comprehensive test example covering all three REST API versioning strategies:

// Test script for API versioning
const axios = require('axios');

async function testAPIVersioning() {
  console.log('Testing REST API Versioning Strategies...\n');
  
  // Test URL Versioning
  console.log('1. URL Versioning Tests:');
  try {
    const v1Response = await axios.get('http://localhost:3000/api/v1/users');
    console.log('✅ V1 URL versioning works');
    
    const v2Response = await axios.get('http://localhost:3000/api/v2/users');
    console.log('✅ V2 URL versioning works');
  } catch (error) {
    console.log('❌ URL versioning failed:', error.message);
  }
  
  // Test Header Versioning
  console.log('\n2. Header Versioning Tests:');
  try {
    const headerV1 = await axios.get('http://localhost:3000/api/users', {
      headers: { 'API-Version': '1' }
    });
    console.log('✅ V1 header versioning works');
    
    const headerV2 = await axios.get('http://localhost:3000/api/users', {
      headers: { 'Accept': 'application/vnd.myapi.v2+json' }
    });
    console.log('✅ V2 header versioning works');
  } catch (error) {
    console.log('❌ Header versioning failed:', error.message);
  }
}

testAPIVersioning();

Conclusion: Implementing REST API Versioning Best Practices

Choosing the right REST API versioning strategy depends on your specific requirements, client base, and architectural constraints. URL versioning offers simplicity and visibility, header versioning provides clean URLs and flexibility, while hostname versioning enables complete version isolation.

Remember these key API versioning best practices:

By following these REST API versioning best practices and implementing the examples provided, you’ll build robust, maintainable APIs that can evolve gracefully while maintaining backward compatibility for your clients.

Frequently Asked Questions

What is the best REST API versioning strategy?

URL versioning is the most popular choice for its simplicity and visibility. However, the best strategy depends on your specific needs: use URL versioning for public APIs, header versioning for clean URLs, and hostname versioning for major architectural changes.

How do you implement API versioning in REST APIs?

You can implement REST API versioning through three main methods: embedding version numbers in URLs (/api/v1/users), using HTTP headers (API-Version: 1), or using different hostnames (v1.api.example.com). Each method has specific use cases and implementation requirements.

What are API versioning best practices?

Key API versioning best practices include: choosing a consistent versioning strategy, maintaining backward compatibility, implementing proper error handling, documenting all versions thoroughly, planning deprecation timelines, and monitoring version usage across your client base.


Need help implementing API versioning in your project? These REST API versioning examples provide a solid foundation for any API versioning strategy. Remember to adapt the code to your specific framework and requirements.

Related Topics: REST API Design, API Documentation, Backward Compatibility, API Gateway, Microservices Architecture

Looking for a Full-Stack JavaScript Developer?

If you're hiring for a full-stack JavaScript developer position and found this article helpful, I'd love to contribute to your team! I specialize in modern JavaScript technologies including Node.js, NestJS, Angular, PostgreSQL, Docker, and Kubernetes, with extensive experience in RxJS, TypeScript, and building scalable web applications.

Let's connect:

  • 📧 Email: jobayer6735@gmail.com
  • Schedule a meeting
  • 💼 View my portfolio and other technical articles on this site

I'm passionate about writing clean, maintainable code and sharing knowledge with the developer community. If your company values technical excellence and continuous learning, I'd be excited to discuss how I can help your team succeed.

Jobayer

© 2025 Jobayer Ahmed. All rights are reserved.