Thanmatt's Blog

Thoughts on code, life, tech, and everything in between

Don't let your database schema become your API contract

5 min read Last updated on

Introduction

You push a simple database change to production. Five minutes later, your phone explodes with Slack notifications:

“The user profile page is broken!”

“Mobile app crashed!”

“I missed my friend’s birthday!”

What happened? You renamed a database field from birth_day to birthday, and your REST API automatically returned the new field name. Your frontend expected birth_day but got birthday instead because of a silly overlooked convention.

Sometimes, a simple backend change becomes a production emergency.

“Okay, fine”, you think. “We’ll coordinate the deployments instead. Backend first, then frontend.”

But even then, there’s still that nerve-wracking window where our frontend is broken in production. And that’s assuming you remembered to update every single unit test that mentions birth_day and hope the pipeline wouldn’t fail.

Spoiler alert:

You probably missed that one.

“Oh, you’re right. We’ll just deploy this during off-peak hours”

Nothing says good software architecture like setting your alarm for 3 AM to rename a database field.

Avoiding this Issue

If you don’t want to stay late or receive angry Slack or Gmail notifications from your boss or worse, your DevOps engineer after the deployment. You might consider redesigning your REST API so you can finally reset your body clock.

The good news? There are proven patterns that can save you from these midnight emergencies. Time to reclaim those normal sleeping hours!

The Design

I’m going to use Express JS for the code examples, but the pattern I’m going to discuss is not limited to Express but to other web frameworks as well.

Imagine your API is designed like this

Database

API Layer

Client/Frontend

It’s a very simple illustration how the data flows until the client.

The problem here is how the API delivers the response back to the client.

Let’s assume you have an endpoint GET /api/v1/user and this endpoint has a middleware (or serializer) that programmatically maps all fields from the User model onto the response.

User

+String first_name

+String last_name

+String phone_number

+String birth_day

+String password

The response:

{
  "first_name": "Foo",
  "last_name": "Bar",
  "phone_number": "+1234567890",
  "birth_day": "October 14, 2023",
  "password": "dajkldhajsdhk" <- Totally secure right?
}

Okay, so I mentioned that the endpoint has a middleware that gets all the fields and maps into a JSON response right? Well, you might want to configure your middleware that can ignore some fields like the password field.

Updated response:

{
  "first_name": "Foo",
  "last_name": "Bar",
  "phone_number": "+1234567890",
  "birth_day": "October 14, 2023"
}

Much better! So that’s one caveat for mapping database fields to the response programmatically. You have to be careful what you’re sending to the client.

But, you’re not done yet. You still have a task to rename that birth_day field. Remember, the JSON response is what your frontend expects right now. If you change something in the backend it may affect your frontend as well.

User_Before

+String first_name

+String last_name

+String phone_number

+String birth_day

+String password

User_After

+String first_name

+String last_name

+String phone_number

+String birthday %% updated column

+String password

The old user model

The updated user model

Response with the updated field

{
  "first_name": "Foo",
  "last_name": "Bar",
  "phone_number": "+1234567890",
  "birthday": "October 14, 2023" <- This was mapped by your middleware
}

Your frontend still expects the birth_day field from the response. But you can stop right here and rewind what you did after you change updated the field.

Without changing anything in the frontend, what if I told you that you could still update that silly birth_day field to birthday without affecting your frontend?

This is where we establish what we call an API contract. This pattern might be familiar to people who have tried or used gRPC’s Protocol Buffers (protoBuf) or GraphQL’s schema.

Our goal here is to make our frontend resistant to change or basically we still want it to make backwards compatible, so we don’t create unnecessary downtime because we changed a field name or any relation to that.

In your backend’s middleware, instead of programmatically map those database model fields into a JSON response, let’s manually map them instead.

Example:

User

+String first_name

+String last_name

+String phone_number

+String birth_day

+String password

Your previous user model

const myMiddleware = (...) => {
  // :: ... your other implementation

  // :: Map the fields manually, establish an explicit contract
  return {
    first_name: user.first_name,
    last_name: user.last_name,
    phone_number: user.phone_number,
    birth_day: user.birth_day
  }
}

User

+String first_name

+String last_name

+String phone_number

+String birthday

+String password

Your updated user model

// :: After DB change: birth_day -> birthday
const myMiddleware = (...) => {
  // :: ... your other implementation

  // :: Map the fields manually, establish an explicit contract
  return {
    first_name: user.first_name,
    last_name: user.last_name,
    phone_number: user.phone_number,
    birth_day: user.birthday  // :: Frontend still gets birth_day!
  }
}

In the example above, I established a contract that should contain first_name, last_name, phone_number, and birth_day regardless what the name of your table fields are.

This looks better now. The frontend still gets the weird, unconventionally named birth_day so you can rest easy now.

Maybe, in this approach, if you want to make it more like a hybrid approach, we can programmatically map the unchanged fields and explicitly map the ones that are changed that affect your frontend.

Yes, this pattern looks a lot like DTOs or serializers because it is. But the point is to treat that transformation layer as your API’s contract, not just boilerplate.

Hybrid approach

// :: After DB change: birth_day -> birthday
const myMiddleware = (...) => {
  // :: ... your other implementation
  const response = { ...user }

  // :: Make sure to remove any sensitive fields
  delete response.password

  // :: Add the old field name for backwards compatibility
  response.birth_day = user.birthday
  return response
}

In this hybrid approach, this lets your frontend gradually transition to the new field name. During the transition period, you return both the old and new field names, giving your frontend team time to update their code without any rush or coordination headaches and we can bid farewell to the ugly birth_day field (like who names that?)

As I’ve mentioned that this also applies to other web frameworks, such as:

  • Django’s serializers (ModelSerializer, custom serializers)
  • Laravel’s API Resources (JsonResource classes)
  • NestJS’s DTOs (with class-transformer decorators)
  • Ruby on Rails’s serializers (ActiveModel::Serializer)

I don’t know with other web frameworks (because that’s all the web frameworks I’ve tried) but I’m 100% sure it’s going to work no matter what web framework you used.

Field names are part of your public API, treat them like a permanent promise.

When to Skip This Pattern?

You can skip this pattern when you’re building internal APIs that only your team uses, or if your project is still in its early stages or MVP phase where you’re still figuring out your data model.

This overhead becomes worthwhile once your API grows and endpoints start popping up everywhere.

For newer projects: START simple and revisit this decision as your system matures.

Next Steps

I’m using REST API as an example, but you get the point.

If you implement this pattern (which I’d strongly recommend), you can make it even more effective by establishing formal API contracts using tools like OpenAPI specs.

Think of an OpenAPI spec as your API’s constitution - it becomes your single source of truth for what fields your API promises to return. In our example, you’d define that your API contract always returns birth_day in the OpenAPI spec, regardless of whether your database field is called birth_day, birthday, or date_of_birth.

This way, your database can evolve freely while your API contract stays rock solid.

Conclusion

If you’re coming from GraphQL or gRPC, it can feel odd that REST APIs don’t come with schemas built in. And yes, that lack of structure can cause headaches as your project grows.

But that doesn’t mean GraphQL or gRPC are automatically “better.” Like everything in software, it depends on your team size, project needs, and long-term goals.

If you’re running a large REST project and struggling with backwards compatibility, don’t assume you need to throw everything out and rewrite in GraphQL. Instead, strengthen what you already have. Tools like OpenAPI specs, JSON Schema, Pydantic (Python), or Zod (TypeScript/JavaScript) can give your REST API the schema-driven safety net you’re missing.

Think of it this way: if your car doesn’t have CarPlay, you don’t immediately buy a new car, you upgrade the stereo. The same applies here: leverage your existing tech stack before jumping to a new paradigm.

Your stack probably has more to give than you think. Sometimes the best architecture decision is simply not rewriting everything.

The Bottom Line

Your API contract should not be your database schema.

Decouple them, use the right tools, and you’ll save yourself from rewrites and 3 AM deploys.

Aethan Matthew
Aethan Matthew

Full stack engineer passionate about clean code, web performance, and sharing knowledge. I write about my journey learning new technologies and building things.