Ruby & Rails

Changing an API without breaking anyone

You cannot make clients upgrade. Every change has to work for consumers who will never read your changelog.

17 December 2025 2 min read Mohammad Aaquib Jawed

An internal API can be changed by changing both sides. A public one cannot: consumers upgrade when they feel like it, and some of them never will.

Additive changes are safe, if clients are tolerant

Adding a field is safe for any client that ignores unknown fields. Adding an optional parameter is safe. A new endpoint is entirely safe.

Removing a field, renaming one, changing a type, adding a required parameter, or narrowing what you accept — all breaking, and all easy to do by accident.

The subtle ones are worth naming: tightening validation breaks clients that were sending something you previously tolerated. Changing default ordering breaks clients relying on it, whether or not you documented it as stable. And changing an error's shape breaks error handling, which is the code least likely to have been tested.

Version when you must, not by default

Versioning everything from day one produces multiple code paths and a maintenance burden before there is a reason for it.

Design so additive change is the normal case, and introduce a version when you genuinely need to break something. URL versioning is the most legible for consumers; header versioning keeps URLs stable. Either works — consistency matters more than the choice.

What does not work is a version that is really a rewrite, where v2 shares nothing with v1 and both must be maintained forever.

Every version you publish is a promise to keep it working. Publish as few as you can and mean all of them.

Deprecate visibly

Removing something needs a path that does not rely on people reading announcements.

A deprecation header on responses using the old behaviour, so it appears in logs. Metrics on which consumers still call it, so you know whether the sunset is safe. Direct contact with the remaining few, because a list of two integrations is a conversation rather than a policy.

And an actual date. "Deprecated" without a removal date is deprecated forever.

Make the contract testable

A schema — OpenAPI or similar — is worth having because it turns "did we break the API?" into a test rather than a judgement.

Contract tests that run the schema against real responses catch the accidental breaking change: the field somebody renamed while refactoring, the type that changed when a column changed. Those are the ones that reach production, because nobody thought of them as API changes at all.

All writing Reply by email