Skip to content

✍️ 필사 모드: API Versioning & Evolution Strategy Complete Guide 2025: Breaking-Change-Free API Evolution, Deprecation, Sunset

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

TL;DR

  • Four versioning strategies: URL Path (/v1/), Header (API-Version: 2024-01-01), Content Negotiation (Accept: application/vnd.api+json;v=1), Query (?version=1)
  • Stripe's approach = date-based: versions like 2024-04-15. The most elegant approach
  • GitHub's approach = REST URL: /v3/repos. Simple but requires migration on every major change
  • GraphQL = no version: evolves via field-level deprecation. Adopted by Twitter, GitHub, Shopify
  • Sunset header: RFC 8594. Sunset: Sat, 31 Dec 2025 23:59:59 GMT — notifies clients of the retirement schedule

1. The Intrinsic Difficulty of API Evolution

1.1 The Fate of Public APIs

"Once an API is public, it lives forever." — Hyrum's Law

Internal code can be refactored freely. External APIs are different:

  • Thousands to millions of clients depend on it
  • Mobile apps require users to update to get the new version
  • Integrated business systems resist change
  • A "brief outage" translates directly into lost revenue

1.2 Types of Changes

ChangeImpactCompatibility
Add new endpointNoneYes
Add new response fieldNoneYes
Add optional fieldNoneYes
Remove response fieldBreakingNo
Rename fieldBreakingNo
Change field typeBreakingNo
Add required fieldBreakingNo
Change error code meaningBreakingNo
Change default behaviorBreakingNo

Rule: Adding is safe, removing/changing is dangerous.

1.3 The Cruelty of Hyrum's Law

"With a sufficient number of users of an API, any observable behavior will be depended on by somebody."

Real examples:

  • Clients that assume response field ordering
  • Clients that parse exact error message text
  • Clients that depend on side effects (e.g., sequential IDs)
  • Assumptions about consistent response time

Conclusion: "It's fine to change what's not in the official docs" is wrong. Every observable behavior is part of the API.


2. Four Versioning Strategies

2.1 URL Path Versioning

GET /v1/users/123
GET /v2/users/123

Pros:

  • Clearest
  • Easy to debug (version visible from URL)
  • Cache-friendly (different URLs = different caches)

Cons:

  • New URL required for each major change
  • Each version maintained as a separate codebase
  • Clients hardcode the URL

Adopters: GitHub (/v3/), Twitter, Stripe (URL is /v1/ but actual version via header)

2.2 Header Versioning

GET /users/123 HTTP/1.1
Stripe-Version: 2024-04-15

Pros:

  • Clean URLs
  • Same resource, different representation
  • Easy incremental migration

Cons:

  • Hard to debug (if headers are hidden)
  • Complex cache handling (Vary header)
  • Client libraries must add headers automatically

Adopters: Stripe (most famous), Azure

2.3 Content Negotiation

GET /users/123 HTTP/1.1
Accept: application/vnd.example.user.v2+json

Pros:

  • HTTP standard (uses Accept header)
  • Different versions via same URL
  • Fits well with HATEOAS

Cons:

  • Complex
  • Client must know the exact MIME type

Adopters: GitHub (optional, Accept: application/vnd.github.v3+json)

2.4 Query Parameter

GET /users/123?version=2
GET /users/123?api_version=2024-04-15

Pros:

  • Simplest
  • Visible in URL (easy to debug)

Cons:

  • Looks like "part of the data" (though it's metadata)
  • Complex cache handling

Adopters: Some simple APIs

2.5 Comparison Table

ApproachClean URLDebuggingCacheAdopters
URL PathNoExcellentExcellentGitHub, Twitter
HeaderExcellentPoorGoodStripe, Azure
Content NegotiationExcellentOKExcellentGitHub (optional)
Query ParamOKExcellentGoodSimple APIs

3. Stripe's Ingenious Date-Based Versioning

3.1 Core Idea

Stripe expresses versions as dates:

  • 2024-04-15 (API behavior on that date)
  • 2023-10-16 (older behavior)
  • 2020-08-27 (5-year-old behavior)

Every change is identified by a date.

3.2 Client Usage

import stripe

# Use account default version
stripe.api_version = "2024-04-15"

# Or per-request
stripe.Charge.create(
    amount=2000,
    currency="usd",
    api_version="2023-10-16"  # Old behavior
)

3.3 Stripe's Secret — the Transformation Layer

The server has exactly one codebase (the latest version).

For each request:

  1. Check client version
  2. Transform request to latest version (forward transform)
  3. Process
  4. Transform response to client version (backward transform)
Client (v2020)[transform] → latest code → [transform]Client (v2020)

Effects:

  • 5-year-old clients still work
  • Code stays current
  • New features reach all users immediately (opt-in)

3.4 Transformation Example

v2020 to v2024 change: added description field to response.

# Transformation function
def transform_to_v2020(response):
    if "description" in response:
        del response["description"]  # v2020 clients don't know this field
    return response

v2020 to v2024 change: amount changed from integer to object.

def transform_to_v2020(response):
    if isinstance(response.get("amount"), dict):
        response["amount"] = response["amount"]["value"]
    return response

3.5 Stripe's Changelog

Each version change is documented precisely:

2024-04-15

  • Added description field to Charge object
  • amount field type changed from integer to AmountObject
  • Default currency is now derived from account settings

Migration guide: ...

This level of transparency is the core of trust.


4. GraphQL's Versionless Evolution

4.1 Core Philosophy

GraphQL doesn't use versions. Instead, it evolves field by field:

  • Add: add a new field — existing clients don't know about it, so no impact
  • Remove: mark with @deprecated, then remove after some time
type User {
  id: ID!
  name: String!
  
  # Deprecated field
  email: String @deprecated(reason: "Use 'emailAddress' instead. Will be removed 2025-12-31")
  emailAddress: String!
}

4.2 Clients Request Exactly What They Need

query {
  user(id: "123") {
    id
    name
    emailAddress  # Request only the new field
  }
}

Existing clients keep requesting email and continue working. New clients use emailAddress.

No over-fetching = adding new fields is free.

4.3 Deprecation Tracking

The GraphQL server collects usage statistics:

  • Which clients are using email?
  • When was the last use?
  • Can it be safely removed?

Apollo Studio and Hasura Cloud provide this functionality.

4.4 GraphQL's Limits

  • Not every change is compatible — type changes, enum value removal, etc., are still breaking
  • Client code generation — needs to be rebuilt with new schema
  • Advanced tooling required — usage tracking, etc.

Adopters: GitHub, Shopify, Twitter, Airbnb


5. Semantic Versioning and APIs

5.1 SemVer Basics

MAJOR.MINOR.PATCH
v1.2.3
  • MAJOR: compatibility broken (breaking)
  • MINOR: compatible + new features
  • PATCH: compatible + bug fixes

5.2 Library vs API

Library: SemVer fits naturally.

  • npm install foo@^1.0.0 → auto-updates 1.x.x

Web API: hard to apply.

  • Clients cannot auto-update
  • The difference between "v1.2" and "v1.3" is meaningful, but "v1.2.3" vs "v1.2.4" is nearly meaningless

Reality: Web APIs usually expose only major versions (/v1, /v2).

5.3 SemVer's Limits

When v2.0.0 ships, every user must migrate. Incremental evolution is hard.

Stripe's approach (date-based) or GraphQL's approach (versionless) is more elegant.


6. Deprecation and Sunset

6.1 Deprecation Stages

  1. Announce: blog, email, changelog
  2. Mark in API: response header or field
  3. Monitor: track usage
  4. Reminder: notify users directly
  5. Sunset: retire (HTTP 410 Gone)

6.2 Deprecation Headers

RFC 8594: the HTTP Sunset header

HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Deprecation: Sat, 31 Dec 2024 23:59:59 GMT
Link: <https://api.example.com/docs/migration>; rel="deprecation"

Meaning:

  • Deprecation: already deprecated (still working)
  • Sunset: retirement schedule
  • Link: migration guide

6.3 Deprecation Messages (Response Body)

{
  "data": {...},
  "warnings": [
    {
      "code": "DEPRECATED_FIELD",
      "message": "Field 'email' is deprecated. Use 'emailAddress' instead.",
      "documentation_url": "https://api.example.com/docs/v2#email-deprecation",
      "sunset_date": "2025-12-31"
    }
  ]
}

6.4 Notifying Users

Technical:

  • Response headers (Sunset, Deprecation)
  • Warnings field in the response body

Communication:

  • Email (registered developers)
  • Blog / changelog
  • Dashboard notice
  • Direct contact (large users)

Stripe: sends automatic email about deprecated APIs in use.

6.5 Sunset Policy Example

UserSunset Period
Free users6 months
Paid users1 year
Enterprise2 years

Larger companies need more time to adapt.


7. Strategies to Avoid Breaking Changes

7.1 Prefer Additive Changes

Wrong change:

- "user_email"
+ "email"

Right change:

+ "email"  // add new field
  "user_email"  // keep old field (deprecated)

Return both fields together. Gives clients time to migrate.

7.2 New Endpoint vs Changing Existing

Bad: change the response shape of existing /users.

Good: new /v2/users endpoint, or /users?format=new.

7.3 Be Careful with Defaults

// v1
{ "page_size": 20 }  // default 20

// v2 — change to 50?
{ "page_size": 50 }  // Breaking! (changes pagination behavior)

Default-value changes are often breaking.

7.4 Optional → Required Is Breaking

- email: string?  // optional
+ email: string   // required

If existing clients don't send email, they fail. Don't add it.

7.5 Is Adding Enum Values Safe?

enum Status {
  ACTIVE,
  INACTIVE,
+ PENDING_REVIEW  // new value
}

Subtle: if the client handles the enum with a switch, a default case is required for the new value. If present, safe; if not, subtle bug.

Advice: adding enum values is technically backward compatible, but requires client code review.


8. Real-World API Evolution Case Studies

8.1 Stripe — the Elegance of Date-Based

  • 10+ years of API evolution
  • Precise date for every change
  • Clients upgrade at their own pace
  • Usage stats + automated notifications

8.2 GitHub — REST to GraphQL

  • REST v3: in operation since 2014
  • GraphQL v4: launched 2017
  • Two APIs run in parallel
  • New features go to GraphQL first

Lesson: leave the old API alone, start a new paradigm separately.

8.3 Twilio — Major Version + Gradual Migration

  • Date prefixes like /2008-08-01/, /2010-04-01/
  • But major changes get new prefixes
  • Old versions kept for years

8.4 Slack — Gradual Deprecation

  • Frequent new methods added
  • Deprecation announced 6 months to 1 year in advance
  • Direct emails to users

8.5 AWS — Almost Never Breaks

  • The S3 API launched in 2006 still works
  • New features are added only; existing features never change
  • Result: the API is inconsistent and complex, but compatibility is perfect

9. Best Practice Checklist

9.1 Design Phase

  • Choose versioning strategy (URL/Header/date)
  • Explicit SLA (how many years of support?)
  • Document deprecation policy
  • Automate changelog

9.2 On Change

  • Is it a breaking change? (verify with checklist)
  • Can it be made additive?
  • Write a migration guide
  • Add Sunset header
  • Notify users
  • Monitor usage statistics

9.3 At Sunset

  • Wait until zero users
  • Final notification
  • Respond with HTTP 410 Gone
  • Remove code (after more time)

10. The Future of API Evolution

10.1 OpenAPI 3.1 + JSON Schema

Automatic compatibility checks via schema:

  • Detect breaking changes automatically on API spec change
  • Auto-generate client code

10.2 AI-Based Migration

  • AI analyzes code → auto-generates migration PRs
  • Automated impact analysis of changes

10.3 Standardizing Contract Testing

  • Tools like Pact and Spring Cloud Contract
  • Enforce contracts between API providers and consumers
  • Verify compatibility in CI

Quiz

1. What is the most common breaking change?

Answer: removing or renaming a response field. If clients are using that field, they break immediately. Safe alternative: add a new field and mark the old one deprecated (return both). Remove after a period. Other common breaking changes: field type changes (string to object), adding required fields, changing defaults, changing the meaning of enum values.

2. What are the advantages of Stripe's date-based versioning?

Answer: (1) Incremental migration — clients adopt new versions at their own pace, (2) Single codebase — the server maintains only the latest version and supports old clients via a transformation layer, (3) Clear changelog — the changes on each date are documented precisely, (4) Easy to test — you can explicitly test a specific-date version. The downside is that implementing the transformation layer is complex.

3. Why doesn't GraphQL use versions?

Answer: In GraphQL, clients request exactly the fields they need, so adding a new field has no impact on existing clients — they don't request it. Field removal is marked with @deprecated, and usage statistics are tracked so removal is safe. Result: an API that evolves forever without versions. Downside: not every change is compatible (type changes, enum value removal, etc.).

4. What is the role of the Sunset header?

Answer: an RFC 8594 standard HTTP header that tells clients "when this resource will be retired". Sunset: Sat, 31 Dec 2025 23:59:59 GMT. Clients can see this header and automatically recognize the migration schedule. Used together with Deprecation and Link headers (migration guide). It lets automated clients respond safely to the retirement schedule.

5. What does Hyrum's Law mean for API design?

Answer: "With a sufficient number of users of an API, any observable behavior will be depended on by somebody." In other words, you can't change things even if they aren't in the official documentation. Response field ordering, exact error message text, response time, ID sequentiality — all become "part of the API". Conclusions: (1) design carefully from day one, (2) consider every observable behavior when changing, (3) explicitly document unintended behavior as "do not depend on this behavior".


References

현재 단락 (1/284)

- **Four versioning strategies**: URL Path (`/v1/`), Header (`API-Version: 2024-01-01`), Content Neg...

작성 글자: 0원문 글자: 13,355작성 단락: 0/284