Writing migration guides that don't break your users
Breaking changes are inevitable. Bad migration guides are not. How to write upgrade and migration docs that get developers from v1 to v2 without rage-quitting your product.
The average developer spends two days migrating between major API versions. That’s not because the breaking changes are complex — it’s because migration guides are written by the people who made the changes, not the people who have to undo them.
You know what changed. Your users know what’s broken. The migration guide is the bridge, and most of them are built with rotten timber.
Why migration guides fail
The failure mode is almost always the same: a list of what changed, not a guide for what to do.
❌ What you usually get:
v2 Breaking Changes:
- The `user_id` field is now `userId` (camelCase)
- The `/api/v1/users` endpoint is removed
- Authentication now uses Bearer tokens instead of API keys
- Rate limits are now per-minute instead of per-hour
This is a changelog, not a migration guide. It tells developers what moved without showing them how to move.
✅ What actually helps:
## Migrating user lookups (5 minutes)
Before (v1):
GET /api/v1/users?user_id=abc123
X-API-Key: your-api-key
After (v2):
GET /api/v2/users?userId=abc123
Authorization: Bearer your-bearer-token
To get a Bearer token, run:
curl -X POST https://api.yourproduct.com/v2/auth/token \
-d "api_key=your-existing-key"
Your existing API key still works — this endpoint exchanges it for a token.
The difference is intent. A changelog records; a migration guide leads.
The anatomy of a migration guide that works
Every migration guide needs five sections. If any is missing, your support queue will fill in the gap.
1. What’s changing and why
Developers will tolerate pain if they understand the reason. “We renamed user_id to userId to align with our new SDK conventions” is more forgiving than a silent rename. If the change makes the API faster, safer, or more consistent, say so. Developers respect engineering decisions when they’re explained.
2. A complete before/after reference
Every breaking change needs its own before/after block — not a summary table, not a bullet list. Actual code. The format is non-negotiable:
// Before (v1)
const client = new APIClient({
apiKey: process.env.API_KEY,
version: 'v1'
});
const user = await client.getUser({ user_id: 'abc123' });
console.log(user.first_name, user.last_name);
// After (v2)
const client = new APIClient({
apiKey: process.env.API_KEY, // same key, no changes here
version: 'v2'
});
const user = await client.getUser({ userId: 'abc123' });
console.log(user.firstName, user.lastName); // fields also renamed
Notice the comment // same key, no changes here. That single line eliminates a support ticket. Any time something looks like it might have changed but didn’t, say so explicitly.
3. A deprecation timeline with hard dates
Vague timelines are unkind. “v1 will be deprecated soon” means nothing to a team planning a sprint. “v1 will return 410 Gone responses on September 1, 2026” means something.
The timeline structure that works:
- Now → June 30: v1 and v2 both active. v1 responses include a
Deprecationheader. - July 1: v1 rate limits drop to 10% of v2. New signups cannot use v1.
- September 1: v1 returns 410. Migration complete.
Hard dates let engineering teams make actual plans. They also force you to commit — which is good. If you’re not confident enough to name a date, you’re not ready to ship a breaking change.
4. An automated migration script
If the change is mechanical — field renames, endpoint URL changes, header format swaps — write the migration script yourself. Don’t make every user write it from scratch.
#!/usr/bin/env python3
# migrate_v1_to_v2.py
# Renames v1 field names to v2 equivalents in your codebase
import re
import sys
from pathlib import Path
RENAMES = {
'user_id': 'userId',
'first_name': 'firstName',
'last_name': 'lastName',
'created_at': 'createdAt',
'/api/v1/': '/api/v2/',
}
def migrate_file(path: Path) -> int:
content = path.read_text()
original = content
for old, new in RENAMES.items():
content = content.replace(f'"{old}"', f'"{new}"')
content = content.replace(f"'{old}'", f"'{new}'")
content = re.sub(rf'\.{old}\b', f'.{new}', content)
if content != original:
path.write_text(content)
return 1
return 0
changed = 0
for path in Path(sys.argv[1] if len(sys.argv) > 1 else '.').rglob('*.js'):
changed += migrate_file(path)
print(f"Updated {changed} file(s).")
A script like this won’t handle every edge case, but it handles 80% of them with zero effort from the user. The remaining 20% — the cases where context matters — are the ones worth calling out explicitly in the guide.
5. A verification checklist
After migrating, developers need confidence that they’re done. A checklist beats prose:
Post-migration checklist:
☐ All requests use Authorization: Bearer <token> (not X-API-Key)
☐ Field names use camelCase throughout (userId, not user_id)
☐ Webhook URLs updated to /v2/webhooks/*
☐ Rate limit budgets updated (now 1000 req/min, not 500 req/hour)
☐ v2 smoke test: GET /api/v2/health returns 200
Deprecation headers before deprecation day
Don’t let developers discover v1 is deprecated by hitting a 410. Add a Deprecation response header the moment you start the deprecation clock:
Deprecation: Sat, 01 Sep 2026 00:00:00 GMT
Sunset: Sat, 01 Sep 2026 00:00:00 GMT
Link: <https://docs.yourproduct.com/migration/v1-to-v2>; rel="deprecation"
This is a published IETF convention. Most HTTP clients can be configured to log or warn on these headers, giving teams automatic notice without anyone having to check your blog.
Log every v1 request on your side too. If you can see who is still using v1 as the sunset date approaches, email them directly. A targeted “hey, you’re still on v1” email three weeks before the cutoff is worth ten blog posts about the migration.
Structure the guide around user workflows, not your change list
Your API changes map to your internal architecture. Your user’s code maps to their workflows. These are rarely the same shape.
❌ Organized by what changed (your perspective):
## Authentication changes
## Endpoint URL changes
## Field name changes
## Rate limit changes
✅ Organized by what users need to do (their perspective):
## Update your authentication flow (~15 min)
## Update user endpoints (~30 min)
## Update webhook handling (~20 min)
## Verify and test your integration (~10 min)
Include estimated times. Developers have sprints to plan. Knowing “this will take about 75 minutes” lets them actually schedule the work.
What to do when the migration is genuinely hard
Sometimes the API change is architectural, not mechanical. A pagination model change, a shift from polling to webhooks, a new data model — these can’t be migrated with a find-and-replace script.
For genuinely hard migrations:
Provide a compatibility shim. A thin adapter that accepts v1 calls and internally translates to v2 buys your users time without freezing your product. Ship it as an optional library: npm install @yourproduct/v1-compat.
Write a worked example end to end. Pick a representative workflow — “fetch a user, update their profile, retrieve their orders” — and show the complete v1 implementation side-by-side with the complete v2 implementation. Not snippets; the full working code.
Host an office hour. A 45-minute open video call the week after announcing the migration does more than any doc. Developers show up with their specific codebases, you answer the edge cases you didn’t anticipate, and you collect the questions you should add to the guide.
Keep the migration guide alive
Migration guides rot. The common mistake is treating them as a one-time document rather than a living resource. As users hit edge cases during migration and report them, update the guide. Add an FAQ section that grows over time:
## FAQ
**Q: I'm using the Python SDK v2.x — do I need to change anything?**
A: No. SDK v2.x handles authentication internally. Only users calling the
HTTP API directly need to update their Authorization header.
**Q: We use pagination tokens from cached responses. Are they still valid?**
A: v1 pagination tokens are not valid in v2. You'll need to re-fetch the
first page and use the new token format.
At GitDocAI, we track which sections of a migration guide get the most traffic and which sections users leave without clicking through. High-exit sections tell you where people give up — those are the sections that need more code examples or clearer explanations.
The test: can a tired developer follow this at midnight?
Migration often happens under pressure. A production incident, a looming deadline, a vendor-forced upgrade. The real test for your migration guide is not “does it cover everything?” but “can a developer who has never seen this change follow it to completion in a high-stress moment?”
Read your guide out loud. If you find yourself thinking “they’ll figure it out from context,” add the context. If a step assumes knowledge the developer might not have, add a prerequisite note. If you can’t test the guide end-to-end in a clean environment, you don’t actually know if it works.
Ship a staging environment that developers can safely test the migration against before touching production. One line in the guide — “run against api-staging.yourproduct.com first” — prevents disasters.
Migration guides are the least glamorous part of API documentation and the most consequential. A broken migration costs your users hours, creates support load, and — at the margin — costs you customers. A great migration guide is the one that gets developers to v2 so smoothly they barely notice they migrated.
If you want to understand how your migration docs are actually performing — which steps developers are bailing on, which FAQ entries get the most hits, which sections drive support tickets — GitDocAI gives you that visibility alongside the tools to keep your docs in sync with every release. Less guesswork, fewer midnight migrations gone wrong.