DevToolBox무료
블로그

API 버전 관리 전략: URL, Header, 콘텐츠 협상

12분by DevToolBox

API versioning is one of the most important architectural decisions in API design. Once clients depend on your API, you can never break backward compatibility without a migration plan. Versioning gives you a way to evolve your API while keeping existing clients functional. This guide covers all major versioning strategies with implementation examples, trade-offs, and deprecation best practices.

Why Version Your API?

  • Breaking changes (renaming fields, changing response shapes) require a new version
  • Adding new required parameters or changing authentication schemes
  • Different clients (mobile, web, third-party) may need to migrate at different times
  • Service SLAs may require months of notice before deprecating endpoints

URL Path Versioning

The version number is embedded directly in the URL path. This is the most widely used approach and is the most visible — you can see the version in every request.

# URL Path Versioning — the most common approach

# Endpoints
GET  /api/v1/users
POST /api/v1/users
GET  /api/v1/users/123
GET  /api/v2/users
GET  /api/v2/users/123

# Express.js implementation
const express = require('express');
const app = express();

// v1 routes
const v1Router = express.Router();
v1Router.get('/users', (req, res) => {
  res.json({ version: 'v1', users: [{ id: 1, name: 'Alice' }] });
});
v1Router.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id, name: 'Alice', email: 'alice@example.com' });
});

// v2 routes (different response shape)
const v2Router = express.Router();
v2Router.get('/users', (req, res) => {
  res.json({
    version: 'v2',
    data: [{ id: 1, firstName: 'Alice', lastName: 'Smith' }],  // different field names
    meta: { total: 1, page: 1 }
  });
});
v2Router.get('/users/:id', (req, res) => {
  res.json({
    id: req.params.id,
    firstName: 'Alice',   // renamed from 'name'
    lastName: 'Smith',
    contact: { email: 'alice@example.com' }  // nested structure
  });
});

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

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

Header-Based Versioning

The version is specified in a request header (custom header or Accept header). URLs remain stable across versions.

# Header-Based Versioning — cleaner URLs, more complex clients

# Using a custom header
GET /api/users
API-Version: 2

# Or using Accept header (media type versioning)
GET /api/users
Accept: application/vnd.myapi.v2+json

# Or using content negotiation per GitHub's approach
GET /api/users
Accept: application/vnd.github.v3+json

# Express.js implementation
const express = require('express');
const app = express();

// Version middleware — extracts version from header
function versionMiddleware(req, res, next) {
  const version = req.headers['api-version'] ||
                  extractVersionFromAccept(req.headers['accept']) ||
                  '1';  // default to v1

  req.apiVersion = parseInt(version, 10);
  next();
}

function extractVersionFromAccept(accept) {
  // Parse: application/vnd.myapi.v2+json → 2
  const match = accept && accept.match(/vnd\.myapi\.v(\d+)\+json/);
  return match ? match[1] : null;
}

app.use(versionMiddleware);

app.get('/api/users', (req, res) => {
  if (req.apiVersion >= 2) {
    return res.json({
      data: [{ id: 1, firstName: 'Alice', lastName: 'Smith' }],
      meta: { total: 1 }
    });
  }
  // v1 response
  res.json([{ id: 1, name: 'Alice Smith' }]);
});

Query Parameter Versioning

The version is passed as a query parameter. Simple but generally less recommended for production APIs.

# Query Parameter Versioning — simple but often considered messy

GET /api/users?version=2
GET /api/users?v=2
GET /api/users?api-version=2  # Azure REST API style

# Python FastAPI implementation
from fastapi import FastAPI, Query
from typing import Optional

app = FastAPI()

@app.get("/api/users")
async def get_users(version: Optional[int] = Query(default=1, ge=1, le=2)):
    if version == 2:
        return {
            "data": [{"id": 1, "firstName": "Alice", "lastName": "Smith"}],
            "meta": {"total": 1}
        }
    # Default v1
    return [{"id": 1, "name": "Alice Smith"}]

# Azure REST API uses api-version query param universally
# GET https://management.azure.com/subscriptions/{id}/resourceGroups?api-version=2024-01-01

Deprecation and Sunset Strategy

The most important part of versioning is a clear deprecation lifecycle. Clients need time to migrate.

# Deprecation and Sunset Strategy
# Critical for managing API lifecycle

# Response headers indicating deprecation
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
Warning: 299 - "API v1 is deprecated. Migrate to v2 by 2026-12-31."

# Express.js deprecation middleware
function deprecationMiddleware(sunsetDate, successorPath) {
  return (req, res, next) => {
    res.set({
      'Deprecation': 'true',
      'Sunset': new Date(sunsetDate).toUTCString(),
      'Link': `<https://api.example.com${successorPath}>; rel="successor-version"`,
      'Warning': `299 - "This API version is deprecated. Please migrate by ${sunsetDate}"`
    });
    next();
  };
}

// Apply to all v1 routes
app.use('/api/v1',
  deprecationMiddleware('2026-12-31', '/api/v2'),
  v1Router
);

# Versioning in OpenAPI/Swagger
openapi: "3.1.0"
info:
  title: My API
  version: "2.0.0"   # API version (not OpenAPI spec version)
servers:
  - url: https://api.example.com/v2
    description: Production v2
  - url: https://api.example.com/v1
    description: v1 (deprecated, sunset 2026-12-31)

GraphQL: Schema Evolution Instead of Versions

GraphQL takes a different approach — instead of versioning the entire API, individual fields are deprecated while new fields are added. This is why GraphQL APIs rarely need explicit versions.

# GraphQL: versioning via schema evolution (no versions needed)

# GraphQL avoids versioning by:
# 1. Adding new fields (backwards compatible)
# 2. Deprecating old fields with @deprecated directive
# 3. Never removing fields until usage is zero

type User {
  id: ID!
  name: String @deprecated(reason: "Use firstName and lastName instead")
  firstName: String!    # new field
  lastName: String!     # new field
  email: String!
  contact: Contact      # new nested type
}

type Contact {
  email: String!
  phone: String
}

# Query: clients using old 'name' field still work
query GetUser {
  user(id: "123") {
    name          # deprecated but still works
    firstName     # new
    email
  }
}

# Monitoring: track deprecated field usage before removal
# Use Apollo Studio or schema field usage analytics

Versioning Strategy Comparison

StrategyProsConsExampleBest For
URL Path (/v2/)Explicit, cacheable, bookmarkable, easy to testVersion in URL feels "dirty" to REST purists/api/v2/usersPublic APIs, open source
Custom HeaderClean URLs, explicit versioningLess visible, harder to test in browsersAPI-Version: 2Enterprise internal APIs
Accept HeaderHTTP standard, content negotiationComplex to implement and testAccept: vnd.api.v2+jsonRESTful purists
Query ParameterSimple, no path changesBreaks REST semantics, bad for caching/api/users?v=2Internal tools, prototypes
No versioning (GraphQL)Single endpoint, schema evolutionComplex schema managementPOST /graphqlGraphQL APIs

Best Practices

  1. Never break an existing versioned API without a migration window of at least 6 months (12+ for enterprise APIs).
  2. Use Sunset headers to communicate the end-of-life date to API clients programmatically.
  3. Maintain changelogs for each API version documenting what changed and why.
  4. Consider semantic versioning: only increment the major version for breaking changes. Minor versions can add new endpoints without incrementing the URL version.
  5. Provide a migration guide with code examples when releasing a new major version.

Frequently Asked Questions

Which API versioning strategy is best?

URL path versioning (/api/v2/resource) is the most widely recommended for public APIs because it is explicit, cache-friendly, and easy to test in browsers and curl. Header versioning is preferred when you want clean URLs and don't mind requiring clients to set headers. Query params are acceptable for internal APIs.

What counts as a breaking change?

Breaking changes include: removing or renaming fields, changing field types (string to integer), making an optional field required, removing endpoints, changing authentication schemes, altering pagination behavior, and changing error response formats. Non-breaking changes: adding new optional fields, adding new endpoints, adding new optional query parameters, adding new HTTP methods to existing endpoints.

Should I version from v0 or v1?

Start at v1 for public APIs. v0 implies instability and discourages adoption. Use a private beta or pre-release flag for truly unstable APIs. If you need to signal a major architectural rethink, go directly to v2. Semantic versioning (v1.2.3) is useful for communicating the extent of changes within a major version.

How long should I maintain deprecated API versions?

For public/third-party APIs: minimum 12 months, preferably 18-24 months. For mobile apps: longer periods because users may not update their apps. For internal APIs: 3-6 months is common. Use analytics to track actual usage — when usage drops to near zero, you can sunset with minimal disruption. Never retire an API that still has significant traffic.

How do I handle versioning for breaking changes in GraphQL?

GraphQL recommends schema evolution over versioning: (1) Add new fields alongside old ones, (2) Mark old fields with @deprecated, (3) Monitor usage of deprecated fields via Apollo Studio or custom metrics, (4) Remove deprecated fields only after usage reaches zero. For truly incompatible changes, run parallel schemas at /graphql/v1 and /graphql/v2.

Related Tools

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

📡HTTP Request Builder{ }JSON Formatter🔓CORS Tester

Related Articles

GraphQL vs REST API: 2026년 어떤 것을 사용해야 할까?

GraphQL과 REST API 코드 예제와 함께 심층 비교. 아키텍처 차이, 데이터 페칭 패턴, 캐싱, 선택 기준을 학습하세요.

API 속도 제한 가이드: 전략, 알고리즘, 구현

API 속도 제한 완전 가이드. 토큰 버킷, 슬라이딩 윈도우, 리키 버킷 알고리즘을 코드 예제와 함께 해설. Express.js 미들웨어, Redis 분산 속도 제한 포함.

Nginx 리버스 프록시 설정: 로드 밸런싱, SSL, 캐싱

Nginx 리버스 프록시 완전 설정: 업스트림 서버, 로드 밸런싱, SSL 종료, 캐싱 프로덕션 배포.