Skip to content
Back to Maintenance & LTS

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 entirely

Common 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.