Field renaming and removal
The four-step migration
Renaming or removing a field is a breaking change. We established that in the first lesson. But here’s the thing: you can make it non-breaking by spreading it across multiple releases instead of doing it all at once.
The process has four steps:
Step 1: Add. Add the new field alongside the old one. Both exist in the response.
Step 2: Keep. Leave both fields in place for a while. Old clients read the old field. New clients read the new field. Nobody breaks.
Step 3: Deprecate. Mark the old field as deprecated. Set a sunset date. Notify clients that it’s going away.
Step 4: Remove. After the sunset date, remove the old field. Clients that haven’t migrated break, but they had months of warning.
Let’s walk through a real example.
Example: renaming created_at to createdAt
Imagine you want to rename created_at to createdAt within v2. This is a field-level breaking change, but we can handle it gracefully.
Step 1: Add both fields.
// Response includes both
{
"id": "book-1",
"title": "Kindred",
"created_at": "2024-01-15T10:30:00", // old
"createdAt": "2024-01-15T10:30:00" // new
} Here’s the transformer:
export function formatBookV2(row: BookRow) {
return {
id: row.id,
title: row.title,
// ... other fields
created_at: row.created_at, // keep for old clients
createdAt: row.created_at, // new name
};
} Both fields contain the exact same value. Old clients read created_at and get the timestamp. New clients read createdAt and get the same timestamp. Everyone is happy.
Step 2: Keep both for the migration period. Document that created_at is deprecated and createdAt is the replacement. Add it to the changelog. Send an email to registered API consumers if you have their contact information.
Step 3: Deprecate. After the migration period (say, three months), check which clients still read created_at. If you have usage tracking, you can see exactly who hasn’t migrated. Reach out to them directly.
Step 4: Remove. After the sunset date, remove the old field:
export function formatBookV2(row: BookRow) {
return {
id: row.id,
title: row.title,
createdAt: row.created_at, // only the new name remains
};
} Clients who migrated are unaffected. Clients who ignored three months of warnings will break, but that’s the tradeoff.
Example: removing a field entirely
What if you’re not renaming but actually removing a field? Say the description field is being moved to its own endpoint.
Step 1: Add the new endpoint /v2/books/:id/description.
Step 2: Keep description in the book response AND available at the new endpoint. Both work.
Step 3: Deprecate description in the book response. Add deprecation notes to the docs. Clients should start using the dedicated endpoint instead.
Step 4: Remove description from the book response. Clients use the dedicated endpoint.
Same four steps. Same gradual process.
When to skip the four steps
For a new major version (v1 to v2), you can make all breaking changes at once. The four-step migration is specifically for changes within a version, where you need to keep backward compatibility while evolving.
Between versions, the old version itself acts as the “keep” step. v1 has the old fields. v2 has the new fields. Both exist simultaneously until v1 reaches its sunset date. That’s the entire migration period built into the versioning model.
The timeline
A typical timeline looks like this:
| Week | Action |
|---|---|
| Week 0 | Add new field alongside old. Deploy. |
| Week 1 | Document deprecation. Notify clients. |
| Week 4 | Check usage of old field. Remind stragglers. |
| Week 12 | Remove old field. Clients had 3 months to migrate. |
Three months is a common migration period. Shorter for internal APIs where you can coordinate directly with teams. Longer for public APIs where external developers need more time to plan, implement, test, and deploy.
Field renaming is one kind of change. But what about when you need to change the entire structure of a response, like wrapping an array in an object or switching from string errors to structured errors? That’s a bigger problem, and it’s what we’ll tackle next.
Exercises
Exercise 1: Add both created_at and createdAt to a response. Verify both contain the same value.
Exercise 2: After a simulated migration period, remove created_at. Verify only createdAt remains.
Exercise 3: Build a migration timeline for renaming author_name to author (object). Write the four steps with dates.
Why include both old and new field names during the migration period?