API Migration
Version and migrate your API without breaking existing clients — deprecate, shadow, then cut over.
Why
Your API has clients you don't control — mobile apps on old versions, third-party integrations, internal services. A breaking change to a single endpoint can break thousands of clients simultaneously. The disciplined approach is versioning + deprecation + a migration window: ship the new version alongside the old, mark the old deprecated (with a Sunset header), give clients a deadline, monitor adoption, then remove the old only after adoption hits 100% or the deadline passes with a communicated extension. This is more work than a big-bang rename, but it's the difference between zero client incidents and a multi-day outage. APIs are contracts; breaking a contract is breaking trust.
When
Whenever a breaking API change is needed. Non-breaking changes (adding a field) don't need a version bump.
How
1. Version the new API: /v2/orders alongside /v1/orders (URL versioning is clearest for clients) 2. Ship v2 with the new behavior; keep v1 working unchanged 3. Add deprecation signals to v1: Sunset: Sat, 31 Dec 2024 GMT + Deprecation: true headers + a Link to v2 4. Document the migration: a changelog with the exact diff and a client migration guide 5. Monitor v1 usage: log every caller (by API key / user-agent); contact the top callers directly 6. Set a sunset date (>=6 months out) and communicate it in the deprecation header + docs + email 7. After sunset date (or 0% v1 traffic for 30d), remove v1 — keep a 404 with a migration link for one more cycle
Example
# v1 and v2 coexist (Express)
app.get('/v1/orders', (req, res) => {
res.set('Deprecation', 'true');
res.set('Sunset', 'Sat, 31 Dec 2024 GMT');
res.set('Link', '</v2/orders>; rel="successor-version"');
// ... old behavior
});
app.get('/v2/orders', (req, res) => {
// ... new behavior (e.g., returns items as array, not {items:[]})
});
# Monitor v1 callers (log middleware on v1)
app.use('/v1/*', (req, res, next) => {
logger.warn({ path: req.path, key: req.apiKey, ua: req.get('user-agent') }, 'v1 deprecation');
next();
});
# Migration guide (docs/api-migration-v1-to-v2.md)
## Breaking: GET /orders response shape
- v1: { "items": [...], "total": 5 }
- v2: [...] (array directly; total is in a header X-Total-Count)
- Client change: replace res.items with res directly; read total from header
# Sunset timeline
- 2024-06-01: v2 ships, v1 deprecated
- 2024-12-31: v1 returns 410 Gone with Link to v2
- 2025-03-31: v1 route removed entirelyCommon Problems
- ⚠Removing v1 the day v2 ships — mobile clients on old app versions break instantly with no recourse.
- ⚠No Sunset/Deprecation headers — clients have no programmatic signal, so they never know to migrate until it breaks.
- ⚠Not monitoring v1 traffic — you remove v1 and discover a partner integration still uses it, breaking a revenue pipeline.
Best Practices
- ✓Ship v2 alongside v1 with deprecation headers (Sunset + Deprecation + Link) — give clients a programmatic signal and a deadline.
- ✓Monitor v1 traffic by caller and contact the top consumers directly — don't rely on clients reading your docs.
- ✓Keep v1 for at least 6 months after v2 ships; remove only when v1 traffic is 0 for 30 consecutive days.