Every Accident in Your API Becomes a Contract
You don’t get to decide which parts of your API are the API. Your users decide that, and they decide it by accident.
We removed a field once. It was called legacy_status, it duplicated a value that already existed under a better name, and it had been marked deprecated in the docs for a year and a half. Nobody read it. We checked the logs, checked the support tickets, checked with the two biggest accounts, and shipped the removal on a Tuesday.
A customer broke on Wednesday. They weren’t reading the field. Their generated client was validating the response against a strict schema, and an unexpected absence failed the same way an unexpected presence would. The field wasn’t part of their integration. The shape of the object was.
That’s the thing about publishing an interface. You think you’re promising the documented behavior. You’re actually promising everything observable: the field order in the JSON, the exact string in the error message someone is regexing, the fact that IDs happen to be sequential integers so a client built pagination on top of them, the 200 you return with an error body because someone in 2019 thought that was easier for browsers. All of it is load-bearing now. You just don’t know which parts until you move one.
So the useful question isn’t what your API does. It’s what your API has revealed. Every accidental detail you expose is a decision you’ve handed to strangers, and they will make it without telling you.
This is why the good advice sounds so boring. Return an object where you might later need a list. Use opaque cursors instead of offsets, not because offsets don’t work but because an opaque string can’t be reverse engineered into a promise. Put a code in your error responses so nobody has to parse your prose. None of it makes v1 better. It makes v1 smaller, which is the only thing that helps in year three.
The other half is that nobody deprecates anything. Not really. You announce a sunset date, you email everyone twice, and on the morning of the cutoff there are still 40 accounts on the old path, and three of them matter enough that the date moves. Then it moves again. Eventually you’re maintaining two code paths permanently and calling it a migration. Every API team I’ve talked to has some version of this, and every one of them describes it as a one-time exception.
The rule that’s actually saved me: before shipping an endpoint, write down what you’d want to change about it in a year. Then either fix it now, or hide it now. Those are the only two options that still exist after the first integration.
Everything else you’re just going to live with.