What Counts as a Breaking Change in Your API
Cleaning up your error handling can break callers. Know which changes are breaking, keep validation rules where they belong, and version from day one so v1 gets deprecated on a schedule instead of broken on a Tuesday.
The demo version of an API handles the happy path. A production API designs its failures, and the moment you start designing them you run into a question most teams answer by accident: is this change breaking?
Say you clean up error handling. Callers that used to get a 500 now get a 409. Empty 404s now carry a Problem Details body. Validation that was missing now rejects bad requests. All of that is better. Some of it is breaking.
Usually breaking
- Removing or renaming a field, in a request or a response.
- Changing a field’s type or meaning.
- Adding a required request field.
- Tightening validation so previously accepted requests now fail.
- Changing which status code or problem
typea known failure returns.
Usually safe
- Adding optional request fields or new response fields.
- Adding endpoints.
- Adding new problem types for failures that didn’t have one.
By that list, the 500 to 409 change is safe in practice. Nobody codes against a 500. The new validation is technically breaking: a request with an empty title used to succeed, and now it doesn’t. On a public API that is a judgment call. Make it deliberately, not after a caller complains.
Where validation rules live
Built-in validation attributes keep rules next to the contract and need no package. FluentValidation keeps rules in separate classes, which helps once rules get complex: conditional rules, rules that depend on other fields, rules that call a service (“does this assignee exist?”).
Use both. Attributes for what a schema can say. Move a rule to a validator the moment it depends on another field or needs a service. A rule that needs the database can go in an async validator or in the handler. Either way, return the same Problem Details shape, so the caller has one thing to handle instead of two.
Signaling deprecation
When a version has to go, tell callers early and tell them in the response itself:
- Asp.Versioning can mark a version deprecated (
HasDeprecatedApiVersion), which adds it to anapi-deprecated-versionsheader. - The
Deprecationheader (RFC 9745) says the resource is deprecated. - The
Sunsetheader (RFC 8594) says when it stops working.
This is why versioning goes in on day one, before you think you need it. When v2 arrives, v1 gets deprecated on a schedule instead of broken on a Tuesday.
The point
Improving an API and breaking an API are not mutually exclusive. Keep the list above somewhere your team can see it, version from the first release, and treat every tightened rule as a decision rather than a fix.