Skip to content
Muhammet Şafak
tr
Asked by: Sinan Answered:

How can I evolve my API in a backward-compatible way without cutting a new version?


Question

We have a REST API consumed by mobile apps. I keep needing to add fields and change some responses, but I can't force the old mobile builds in the wild to update — people stay on old versions for months, even years. How can I evolve my API in a backward-compatible way without cutting a new `/v2`, `/v3` every time? Which changes are safe and which aren't, and what should I do when I genuinely have to make a breaking change?

Answer

Short answer: you can make most of your changes without cutting a new version — the rule is simple: add, never remove/rename/retype, and rely on the client ignoring unknown fields (a tolerant reader).

Short answer

For the unavoidable breaking change, pin the version via a header, not the URL.

Why

  1. Rule one: add, never remove or repurpose. Adding a new field to a response is backward-compatible; the old client ignores it. The breaking move is removing an existing field, renaming it, or changing its type/meaning (e.g. turning a string into an object).

What to do

  1. Verify the tolerant-reader assumption. Many JSON parsers ignore unknown fields, but a client doing strict schema validation (or a code-generated DTO) can blow up on a new field. Test that your mobile client actually tolerates it with an old build — don’t just assume it.

  2. Make new request fields optional with safe defaults. An old client that never sends the new field must still work as before. And never tighten validation on existing input: loosening a rule is safe, tightening it breaks the old clients already in the wild.

  3. Watch enums and status semantics. Adding a new value to an enum looks innocent but an old client may not recognize it and error out; gate new values or keep a compatible fallback. Likewise, don’t change the meaning of HTTP status codes or error codes — clients are branching on them.

  4. For a genuine breaking change, pin via a header, not the URL. Use a date/version header like Stripe does (Stripe-Version: 2026-09-30.endive); each client pins to the version it registered with, and you serve the old and new shapes side by side on the server. That lets you move forward without breaking the old build in the field.

    GET /orders/42
    API-Version: 2026-10-01
    
    # The server returns the old or new shape based on this header;
    # if absent, pin the client to the first version it ever saw.
  5. Retire nothing without telemetry. Measure the client versions in the wild (User-Agent, version header, usage per API key). Only retire a field or an old shape once you can see no active build uses it. Without measurement, “is it safe to remove?” has no answer.

Bottom line: personally I’d make 90% of day-to-day changes with no new version at all, through additive-only + tolerant-reader discipline: only add, never touch existing fields, keep new inputs optional. When an unavoidable breaking change arrives, I’d introduce a version header rather than /v2, serve both shapes during a transition window, and drop the old one only once telemetry approaches zero. On mobile, old builds live for years — so I’d put the pinning mechanism in place today, before I truly need it.

Related Reading

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind