You inherited a codebase where the API returns database column names verbatim: user_id, created_at, is_verified, straight from Postgres into the JSON response with no translation layer. Now the frontend team wants a consistent camelCase API surface, and you need to get there without breaking every existing client that depends on the current field names.
Here's a migration path that doesn't require a flag day.

Photo by Markus Spiske on Pexels
Step 0: Confirm the migration is actually worth doing right now
Before writing any code, check whether this rename is solving an active problem or just an aesthetic one. If the current snake_case API is causing real friction (frontend developers writing awkward destructuring to work around it, or a shared component library that can't be reused across a camelCase frontend and this API), the migration pays for itself. If it's purely "this looks inconsistent with our other services," weigh that against the transition-period complexity below. Not every naming inconsistency is worth the migration risk to fix.
Step 1: Add a serialization layer, don't touch the database
Resist the urge to rename database columns as your first move. The database schema and the API response shape don't have to match, and decoupling them is the actual fix. Add a serializer or transform function between your query layer and your response layer that converts snake_case row data to camelCase response fields.
function serializeUser(row) {
return {
userId: row.user_id,
createdAt: row.created_at,
isVerified: row.is_verified,
};
}
This is mechanical, repetitive work, exactly the kind of thing that benefits from a converter rather than hand-typing each field. Running your full list of column names through a Case Converter in bulk mode gives you the camelCase equivalents to paste directly into the serializer, instead of retyping (and occasionally mistyping) each one by hand.
Step 2: Ship both field names during a transition window
Rather than replacing user_id with userId in a single deploy, return both for a defined transition period:
return {
user_id: row.user_id,
userId: row.user_id,
// ...
};
This costs a slightly larger payload temporarily but means existing clients keep working on the old field name while new clients (or updated versions of existing ones) can switch to the new one whenever they're ready, not on your deploy schedule.
Step 3: Add a deprecation warning, not a breaking change
If your API supports response headers or a changelog mechanism, flag the old field names as deprecated with a target removal date. A Deprecation header or a note in API documentation gives consumers a concrete timeline instead of an open-ended "eventually we'll remove this."
Step 4: Track actual usage before removing anything
Log which field name variant each request actually consumes, if you can determine that from the client, or at minimum track which API versions or client identifiers are still active. Removing the old field names before confirming nothing depends on them anymore is how a "safe" cleanup migration turns into an incident.
Step 5: Remove the old names only after the transition window closes
Once you've confirmed no active client depends on the snake_case field names (via logging, a deprecation deadline that's passed, or direct confirmation from consuming teams), remove them from the serializer. At this point the database still has whatever column names it always had, but the API contract is now consistently camelCase, and the migration required zero changes to the actual schema or a single flag-day cutover.
Handling nested objects and arrays
Real API responses rarely have flat structures. If a user object contains a nested billing_address object, or a list of order_items, the serializer needs to recurse into those structures rather than only converting top-level keys. A common mistake is writing a serializer that handles the top level correctly and misses that the nested objects still have snake_case keys, which produces an API response that's inconsistent within itself. Test the serializer against your deepest, most nested real response shape, not just a flat example object, before considering the migration complete.

Photo by Michael Burrows on Pexels
What this approach avoids
The naive version of this migration is a single PR that renames database columns directly and updates every query at once. That's a much larger, much riskier change: every migration script, every raw SQL query, every ORM model needs updating simultaneously, and there's no gradual rollback path if something breaks. Keeping the database columns unchanged and doing all the translation in a serialization layer means the actual risky surface area (the database schema) never moves at all.
If you want the broader reasoning behind why teams end up with mismatched naming conventions in the first place, and how to decide on a convention before this kind of migration becomes necessary, there's a longer guide on choosing a naming convention that covers the decision framework in more depth.
References
- PostgreSQL's official site for documentation on identifier naming and case folding rules.
- Sequelize and TypeORM both support automatic field name transformation between database and application layers, which can replace a hand-written serializer once you're past the transition period.
- Semantic Versioning for guidance on how field renames and deprecations should be reflected in your API's versioning scheme.
-
MDN's web docs cover the
DeprecationHTTP header semantics if you want to signal field-level deprecation formally.
How long to keep the transition window open
There's no universal answer, but a reasonable default is tying the transition window to your API versioning policy rather than picking an arbitrary date. If you support the previous major API version for six months after a new one ships, give the dual-field transition the same window. If your API has no formal versioning and clients update whenever they update, lean toward a longer window and active usage monitoring rather than a fixed date, since you genuinely don't know who's still depending on the old field names until you check.
Communicating the migration to API consumers
Even an internal-only API benefits from an explicit announcement rather than a silent change. A short note in your API changelog, or a message in whatever channel your API consumers actually watch, stating what's changing, why, and when the old fields will be removed, turns a migration that could otherwise cause a surprising breakage into a scheduled, low-drama event. This matters more the more consumers your API has, but it's cheap enough to do even for a single internal client, and it builds the habit for when the API has more consumers later.
This migration pattern isn't specific to naming conventions. It's the general shape of any breaking API change: decouple the internal representation from the external contract, ship both during a transition, measure actual usage, then remove the old path once it's confirmed safe. Once you've done it once for a naming migration, the same five-step shape applies just as well to deprecating an old authentication scheme, changing a pagination format, or retiring any other public contract your API has made. Learning the pattern here is worth more than the specific naming fix, since you'll reuse it the next time any breaking change needs to ship without a flag day.
Top comments (0)