Skip to main content

Command Palette

Search for a command to run...

Evolving APIs Without Breaking Clients.

Updated
M

Learner, Love to make things simple, Full Stack Developer, StackOverflower, Passionate about using machine learning, deep learning and AI

Why API changes fail in production and how experienced teams avoid it.

Read first article from series is here.

Introduction: APIs don’t belong to you anymore

The moment you publish an API, it stops being just your code.

It becomes:

  • Someone else’s dependency

  • Someone else’s release risk

  • Someone else’s production problem

Most API failures don’t come from load or latency.
They come from unexpected change.

If backward compatibility is hard in general, APIs make it harder—because your consumers are often invisible to you.


The uncomfortable truth about API consumers

You rarely know:

  • Who is using your API

  • How often they call it

  • What assumptions they made

  • How quickly they can upgrade

Even inside the same company:

  • Teams move on

  • Ownership changes

  • Documentation becomes outdated

Designing APIs means designing for uncertainty.


What an API contract really is

An API contract is not just:

  • Endpoint URL

  • HTTP method

  • Request and response fields

It also includes:

  • Field meanings

  • Validation rules

  • Default values

  • Error behavior

  • Ordering and timing

Breaking any of these can break clients—even if the JSON still “looks valid”.


Example 1: The hidden breaking change

Original response:

{
  "total": 5
}

Later change:

{
  "total": "5"
}

Everything still parses.

But:

  • A strongly typed client fails

  • Calculations behave incorrectly

  • Bugs appear far from the API

Lesson:
Data type changes are breaking changes—even if they seem harmless.


Additive changes: the safest way forward

The safest API evolution strategy is addition.

You can safely:

  • Add optional fields

  • Add new endpoints

  • Add new error codes

Clients that don’t know about them simply ignore them.

Example 2: Adding instead of modifying

Instead of changing:

{
  "amount": 1000
}

Add:

{
  "amount": 1000,
  "pricing": {
    "value": 1000,
    "currency": "INR"
  }
}

Old clients keep working.
New clients get richer data.

This is boring design.
And boring is good.


When behavior changes, version the API

If the meaning of a response changes, versioning is not optional.

Example 3: Business rule change

Originally:

  • Order is created even if stock is low

Later:

  • Order fails if stock is insufficient

Same endpoint. Same request. Different outcome.

This is a breaking change—even if the response schema is unchanged.

Correct approach:

/api/v1/orders → old behavior
/api/v2/orders → new rules

Versioning is not about structure.
It’s about behavior.


Common API anti-patterns (avoid these)

Anti-pattern 1: Overloading fields

Using one field to represent multiple concepts.

{
  "status": "FAILED"
}

Does it mean:

  • Validation failed?

  • Payment failed?

  • System failed?

Clients guess.
Guessing leads to bugs.


Anti-pattern 2: Making optional fields mandatory

Today:

{
  "email": "user@test.com"
}

Tomorrow:

{
  "email": "user@test.com",
  "phone": "9999999999"
}

If phone becomes required:

  • Old clients fail validation

  • Deployments break unexpectedly

Rule:
Once optional, always optional—at least in the same version.


Anti-pattern 3: Silent validation tightening

Validation changes break more clients than schema changes.

Example:

  • Previously allowed empty string

  • Now rejects it

Same field. Same API. Different outcome.

This breaks clients silently.


Be liberal in what you accept

APIs should be forgiving at the edges.

Accept:

  • Missing optional fields

  • Extra fields you don’t understand

  • Slightly older formats

Reject only when:

  • Data is invalid

  • Security is at risk

  • Business rules truly require it

Strict input + no versioning = production pain.


Error handling is part of the contract

Changing error behavior is a breaking change.

Example 4: Error response change

Old:

{
  "error": "INVALID_REQUEST"
}

New:

{
  "errors": [
    { "code": "INVALID_REQUEST" }
  ]
}

Cleaner? Yes.
Backward compatible? No.

Clients parse errors too.


How experienced teams evolve APIs safely

They do three things consistently:

  1. Design APIs as public contracts

  2. Assume clients upgrade slowly

  3. Measure usage before removing anything

They treat APIs like products—not internal functions.


A simple checklist before changing an API

Before you change an API, ask:

  1. Will old clients still work?

  2. Are we changing behavior or just adding data?

  3. Can this be done additively?

  4. Do we need a new version?

  5. How long will we support the old one?

If the answer is unclear, don’t change it yet.


Final thought: APIs fail at the boundaries

Most systems don’t fail at the core logic.

They fail at the boundaries—where assumptions meet reality.

APIs are those boundaries.

Design them carefully, evolve them slowly, and never forget:
Once an API is used, it no longer belongs only to you.

More such articles:

https://medium.com/techwasti

https://www.youtube.com/@maheshwarligade

https://techwasti.com/series/spring-boot-tutorials

https://techwasti.com/series/go-language

Backward Compatibility

Part 4 of 5

Hard-earned lessons on why breaking changes cause real damage in production, and how experienced engineers design new features that respect the past.

Up next

Backward Compatibility Is Not Optional.

Why real software must respect its past.

More from this blog

T

techwasti

276 posts

TechWasti is a community where we are sharing thoughts, concepts, ideas, and codes.