API & Contract Design
An API is a promise. Once a consumer depends on it, you cannot break it quietly. Design contracts to be clear, consistent, and hard to misuse, because the shape you ship is the shape you will support for years. A good API makes the right call obvious and the wrong call hard.
A contract is everything a caller can see: the routes, the request and response shapes, the status codes, the error format, and the rules about what is required and what each thing means. Design it from the consumer's side: predictable, consistent, and self-describing. Treat it as a stable surface that evolves carefully, not one that changes without warning.
APIs are also a security boundary. Every endpoint is an entry point an attacker will probe (see Trust Boundaries). So contract design and security design are the same task: validate input, authenticate and authorize, never return too much data, and never leak internals in errors. The goal is a clean contract that also fails safe.
Design for the consumer
- DoBe consistent across the API. Naming, casing, pagination, filtering, error shape, and date and number formats follow one convention everywhere.
- DoUse HTTP semantics correctly: the right verbs, the right status codes, and idempotent GET, PUT, and DELETE, so retries are safe.
- DoReturn a stable, documented error shape (a code plus a safe message), not raw internals. Document the contract (for example, OpenAPI) as the source of truth.
- DoAccept and return explicit, validated DTOs, with required and optional fields clearly defined. Do not use loose pass-through objects.
- ConsiderDesigning endpoints around what the consumer needs (use cases) instead of exposing the database schema directly.
- Do notLeak internal model field names, database columns, or implementation details into the public contract. They become impossible to change later.
Make the contract safe
- AlwaysValidate every request against the contract at the boundary, and authenticate and authorize every endpoint by default (see Authentication & Authorization).
- DoReturn only the fields the caller needs and is entitled to. Shape responses on purpose; never serialise the whole entity.
- DoUse explicit request models to avoid over-posting and mass-assignment, so a caller cannot set fields they should not (for example, role, tenant, price).
- ConsiderRate limits and request-size limits as part of the contract for public endpoints, to reduce abuse.
- NeverTrust identity, role, tenant, or price from the request body or a client-set field. Derive privileged values on the server.
- NeverReturn internal details (stack traces, SQL, secrets, file paths) in an API response to an external caller.
[HttpPost] public Customer Create(Customer c) { db.Insert(c); return c; }
This allows mass-assignment: a caller can set Id, TenantId, Role, or KycStatus directly. The response also serialises every column, including internal and sensitive fields. The contract becomes whatever the table happens to be.
[HttpPost] public CustomerResponse Create(CreateCustomerRequest req) {
var c = new Customer { TenantId = User.GetTenantId(), Name = req.Name };
db.Insert(c);
return CustomerResponse.From(c); // only safe, intended fields
}
The caller can set only what the request DTO allows. Privileged values come from the server. The response exposes a deliberate, stable shape.
Self-review checklist
- AskIs this endpoint consistent with the rest of the API's conventions?
- AskCould a caller set a field they shouldn't (role, tenant, price) by over-posting?
- AskDoes the response return only what the caller needs and may see?
- AskIs anything in this contract an internal detail I'll regret exposing later?