An API endpoint can work perfectly and still be badly designed.
It accepts today’s request. It returns today’s response.
Then a mobile client ships, a webhook retries, a field becomes optional, a list grows, and a second tenant role appears.
The handler still “works.”
The contract no longer does.
Validate at the boundary
TypeScript types disappear at runtime.
Network input remains untrusted.
I validate:
- shape;
- allowed values;
- lengths and limits;
- identifiers;
- dates and time zones;
- business meaning;
- unknown keys where they create risk.
Then I normalize once.
If phone numbers, emails, currency codes, or empty strings can appear in several forms, the domain should not rediscover that ambiguity in every service.
Identity is part of the contract
The request body should not decide who owns the operation.
The server derives actor and tenant context from verified identity, then checks the target resource.
That decision belongs inside the protected operation—not only in a page guard or hidden button.
For every endpoint, I want to answer:
- Who is acting?
- In which tenant or account?
- On which resource?
- With which role?
- Is the operation allowed now?
Errors should help the caller act
“Something went wrong” is not an API contract.
A useful error has a stable shape:
type ApiError = {
error: {
code:
| "VALIDATION_FAILED"
| "NOT_FOUND"
| "CONFLICT"
| "RATE_LIMITED"
| "DEPENDENCY_UNAVAILABLE";
message: string;
fields?: Record<string, string>;
requestId: string;
};
};
The human message can improve.
The code lets the client choose behavior.
Do not leak database details, stack traces, secrets, or another tenant’s resource existence.
Retries need idempotency
Networks fail in ambiguous ways.
A client may not know whether a payment, order, email, or job was accepted.
If retrying creates a duplicate, the endpoint is fragile.
For retryable creation or side effects, consider:
- idempotency keys;
- unique operation constraints;
- persisted result replay;
- safe status transitions;
- provider event IDs;
- deduplicated job enqueueing.
“The frontend disables the button” is not idempotency.
Timeouts and cancellation are product behavior
Every external dependency can stall.
Define:
- timeout;
- retry policy;
- backoff;
- cancellation;
- fallback;
- user-visible state;
- alerting threshold.
Do not retry every error.
Authentication failures, validation failures, and many conflicts need a decision—not another identical request.
Pagination must stay stable
Returning every record works until it does not.
Offset pagination is simple but can shift under inserts and deletes.
Cursor pagination can be more stable when the ordering is explicit and the cursor is opaque.
Whichever model you use, define:
- deterministic sort;
- page limit;
- cursor or offset rules;
- filters;
- empty response;
- total-count behavior;
- maximum cost.
Never let an unbounded query become a public feature.
Webhooks are hostile inputs with delivery semantics
A webhook endpoint needs:
- signature verification on the raw body where required;
- timestamp or replay protection;
- event deduplication;
- tenant/provider mapping from server truth;
- fast acknowledgement;
- background processing where appropriate;
- safe logs;
- retry-aware state changes.
Provider delivery is often at least once.
Your side must expect repetition.
Compatibility needs a policy
Not every field change deserves a new API version.
But every public contract needs rules:
- adding optional fields;
- deprecating fields;
- changing enums;
- changing default behavior;
- removing endpoints;
- version support window;
- migration communication.
I prefer additive change when possible.
When behavior must break, make the transition explicit and observable.
Test the contract, not only the function
Useful coverage includes:
- valid request and response;
- malformed input;
- unauthenticated actor;
- unauthorized actor;
- cross-tenant resource;
- conflict and duplicate;
- dependency timeout;
- retry;
- pagination edge;
- webhook replay;
- compatibility fixture.
The contract lives where systems meet.
That is where the tests should meet it.
My design rule
An API should make the correct path easy, the incorrect path predictable, and the dangerous path difficult.
That requires more than a route and a controller.
It requires a boundary the caller can depend on.
For tenant ownership, read secure multi-tenant SaaS foundations. For code entering production, read my release review checklist.
If an API keeps breaking clients as it grows, send me one request, response, and failure case.
- #API Design
- #Backend
- #TypeScript
- #Reliability
- #SaaS

Continue reading
Related field notes on engineering, SaaS, freelancing, and building dependable products.