✍️ 필사 모드: API Versioning & Evolution Strategy Complete Guide 2025: Breaking-Change-Free API Evolution, Deprecation, Sunset
EnglishTL;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
| Change | Impact | Compatibility |
|---|---|---|
| Add new endpoint | None | Yes |
| Add new response field | None | Yes |
| Add optional field | None | Yes |
| Remove response field | Breaking | No |
| Rename field | Breaking | No |
| Change field type | Breaking | No |
| Add required field | Breaking | No |
| Change error code meaning | Breaking | No |
| Change default behavior | Breaking | No |
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
| Approach | Clean URL | Debugging | Cache | Adopters |
|---|---|---|---|---|
| URL Path | No | Excellent | Excellent | GitHub, Twitter |
| Header | Excellent | Poor | Good | Stripe, Azure |
| Content Negotiation | Excellent | OK | Excellent | GitHub (optional) |
| Query Param | OK | Excellent | Good | Simple 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:
- Check client version
- Transform request to latest version (forward transform)
- Process
- 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
descriptionfield toChargeobjectamountfield type changed from integer to AmountObject- Default
currencyis now derived from account settingsMigration 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
- Announce: blog, email, changelog
- Mark in API: response header or field
- Monitor: track usage
- Reminder: notify users directly
- 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 scheduleLink: 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
| User | Sunset Period |
|---|---|
| Free users | 6 months |
| Paid users | 1 year |
| Enterprise | 2 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
- Stripe API Versioning — explains the date-based approach
- GitHub API v3 to v4 — GraphQL migration
- RFC 8594 — Sunset Header
- Hyrum's Law
- OpenAPI 3.1 — API specification standard
- Pact — Contract testing
- GraphQL Best Practices
- API Stylebook — collection of per-company API guides
- Twilio API Versioning
- Building Evolvable Web APIs — Glenn Block et al.
- API Change Management at Stripe
현재 단락 (1/284)
- **Four versioning strategies**: URL Path (`/v1/`), Header (`API-Version: 2024-01-01`), Content Neg...