Migration Path
Provide a concrete, copy-pasteable path from the deprecated thing to its replacement.
Why
Telling users to "migrate to v2" without a migration path is telling them to do unpaid work with no map — most won't, and you'll break them at sunset. A migration path is a document that shows the exact diff: the old call, the new call, the behavioral differences, and a codemod or script that does the mechanical part. The path must be concrete enough that a user can follow it in 10 minutes without reading your source. The best migration paths include an automated codemod so the user runs one command and is done — this converts migration from a multi-day project into a 5-minute task and is the single highest-leverage thing you can do to drive adoption of the new API.
When
For every deprecation. Write the migration path before you ship the deprecation signal.
How
1. Write a migration guide doc: docs/migrate-<old>-<new>.md 2. Show the before/after for each changed call — exact code, not prose 3. List every behavioral difference (response shape, error codes, side effects) — the surprises 4. Provide a codemod if the change is mechanical: npx @your-scope/migrate-v1-to-v2 5. Provide a compatibility shim if useful: a thin adapter that maps old calls to new, for gradual migration 6. Include a verification step: how the user confirms the migration worked (a test, a diff of responses) 7. Link the migration guide from the deprecation header, the changelog, and every communication channel
Example
# docs/migrate-v1-v2.md
## GET /v1/orders -> GET /v2/orders
### Before
const orders = await fetch('/v1/orders').then(r => r.json());
// orders = { items: [...], total: 5 }
const list = orders.items;
### After
const res = await fetch('/v2/orders');
const orders = await res.json();
// orders = [...] (array directly)
const list = orders;
const total = parseInt(res.headers.get('X-Total-Count'));
### Behavioral differences
- Response is an array, not { items, total }
- total is in the X-Total-Count header, not the body
- 404 now returns { error: "not_found" } instead of "Not Found" string
### Codemod (automated)
npx @ez4code/migrate-v1-to-v2 src/
# Rewrites: fetch('/v1/orders') -> fetch('/v2/orders')
# res.items -> res
# res.total -> parseInt(res.headers.get('X-Total-Count'))
### Verify
# Run your test suite; the codemod preserves behavior.
# Diff a sample response: curl /v1/orders vs /v2/orders (after shim)Common Problems
- ⚠A migration guide that says "use v2" with no before/after — users have to read both API docs and diff them themselves.
- ⚠No codemod for a mechanical change — users hand-edit 200 call sites, introduce typos, and blame your API for the bugs.
- ⚠Not documenting behavioral differences — the user migrates, tests pass, and then a 404 error shape change breaks their error handler in prod.
Best Practices
- ✓Show exact before/after code for every changed call — prose is not a migration path, copy-pasteable code is.
- ✓Provide a codemod for any mechanical change — one command vs 200 manual edits is the difference between migration and abandonment.
- ✓Document every behavioral difference (error shapes, side effects, defaults) — the surprises are what break production after tests pass.