API Design Signals

Green flags show consumer-first, guarantee-aware contract thinking. Red flags reveal dogma, missing failure answers, and promises nobody decided to make.

Green flags

Begins with consumers and their tasks, not with endpoints.
Endpoints derived from consumer tasks survive contact with real clients; endpoints derived from tables get reworked within a quarter.
Extracts requirements before sketching a single path.
The contract is a promise about behavior; you cannot promise what you have not asked about — load, consistency, evolution, failure.
Picks an API style from consumer environment and trade-offs, not fashion.
REST, gRPC and GraphQL each buy something and cost something; naming the price is what separates a decision from a preference.
Treats internal APIs as contracts with real consumers.
The consumer being a colleague changes the communication channel, not the blast radius of a breaking change.
Models lifecycles as explicit state machines with rejected invalid transitions.
created → delivered without paid in between is a bug the contract can make impossible instead of a bug support discovers.
Designs the error model as carefully as the success path.
Clients spend most of their defensive code on failures; a stable code taxonomy is the difference between branching and string-matching.
Bounds every list endpoint from day one.
Retrofitting pagination is a breaking change; shipping it on day one costs an afternoon.
Plans retries and idempotency for every unsafe operation.
The network will lose responses; whether a retry double-charges someone is decided at design time, not at incident time.
Treats a timeout as an unknown outcome, not a failure.
The request may have committed after the client gave up; contracts that acknowledge this ambiguity give clients a safe recovery path.
Documents rate limits, quotas and 429 behavior as part of the contract.
A limit clients can observe and back off from is throttling; a limit they discover in production is an outage on their side.
States exactly what a change breaks before shipping it.
The additive-safe list vs the breaking list (remove, rename, type change, meaning change) is checkable — guessing is optional.
Measures per-consumer usage before removing anything.
If mobile clients at 12% still read the field, it is not removable — telemetry turns deprecation from a gamble into a schedule.

Red flags

'POST is not idempotent, so retries are impossible.'
Idempotency is a property you design in — an Idempotency-Key with stored result replay makes any POST safely retryable.
'200 with error=true in the body is fine everywhere.'
It blinds every intermediary — caches store failures, monitors see success, retry logic never fires; status codes are the machine-readable channel.
'We can break internal APIs anytime.'
Internal consumers deploy on their own schedules too; the difference from public is who you apologize to, not whether things break.
'GraphQL solves N+1.'
GraphQL relocates N+1 from the client to your resolvers, where every flexible query can trigger it — batching and cost control are now your job.
'Version everything' — or 'never version anything.'
Both dodge the real question: which changes are additive-safe and which break clients. Versioning is a targeted tool, not a policy switch.
'The response model is just the database row.'
It welds your storage schema to every consumer — the day you normalize a table, you break clients that never cared about your tables.
'We'll add pagination later if the list gets big.'
Later, adding it breaks every client that assumed the full array — and the endpoint has already taken production down once by then.
'REST means every operation must become CRUD on a noun.'
Cancel, approve, retry are domain concepts; contorting them into PATCH side effects hides semantics that deserve explicit modeling.
'Webhooks arrive exactly once and in order.'
Delivery is at-least-once over a network you do not control — duplicates and reordering are normal operation, not edge cases.
'A timeout means the request failed.'
The server may have committed after the client hung up; treating timeout as failure produces blind retries and duplicate side effects.
'The docs can lag — the code is the contract.'
Consumers integrate against the docs, not your source; whatever the docs promise is what you are on the hook for, drift included.