The first API contract is usually written with incomplete knowledge. Durability comes from making assumptions visible, constraining change and giving clients predictable failure behaviour.
Specify the contract at the boundary
Validate path, query and body inputs before business logic. Return stable error shapes. Treat null, omitted and empty values deliberately. Generate types where practical, but remember that a TypeScript type disappears at runtime.
type CreateCommand = {
idempotencyKey: string;
customerId: string;
amountMinor: number;
};
const existing = await commands.find(input.idempotencyKey);
if (existing) return existing.result;
return commands.executeOnce(input);Prefer additive change
Adding an optional response field is normally safer than changing the meaning of an existing one. When semantics must change, version the contract with a clear migration window rather than keeping two undocumented behaviours behind one route.
Make authentication and authorisation separate decisions
Authentication establishes an identity. Authorisation decides whether that identity can perform this action on this resource. Log the decision context without logging secrets or unnecessary personal data.
Observe outcomes, not only requests
Latency and status codes are a start. Add correlation identifiers, dependency timings and domain outcomes that explain whether the operation achieved its purpose. Alerts should point toward a failing responsibility rather than announce that “the API is down.”