HTTP Foundations: Methods, Status Codes, and Representation Boundaries
Design a predictable request and response contract by distinguishing resources, representations, safe methods, and retryable operations.
On this page
Key takeaways
- A successful HTTP exchange is not proof that the business operation succeeded.
- Safe and idempotent describe semantics, not whether a particular implementation happens to write data.
- Validate content type, payload shape, and authorization at separate boundaries.
Model the resource before the handler
HTTP transfers representations of resources. /orders/42 identifies an order; the JSON returned by a GET is one representation at a point in time. A client should not need to know whether the server used a database, a cache, or another service to produce it. Define the public contract independently of internal storage columns so that migrations do not accidentally become breaking API changes.
Separate transport success from domain success. A TCP connection can succeed while HTTP returns a failure, and an HTTP 200 response can still contain a poorly designed application error envelope. Prefer status codes that communicate the outcome, then include a stable machine-readable error code and a human-readable message. Never require clients to parse changing English messages to decide whether to retry.
Understand safe and idempotent methods
GET and HEAD are safe: the client is not requesting a state change. Logging or metrics can occur, but a GET endpoint that deletes records violates the contract. PUT and DELETE are idempotent: repeating the same intended request should have the same intended effect as performing it once. Responses need not be identical; a second DELETE can return 404 after the first returned 204.
POST is not inherently idempotent. Creating a payment twice is not equivalent to creating it once. If clients can retry a creation request, define an idempotency-key contract backed by durable deduplication and a payload comparison. PATCH is not automatically idempotent either: setting a quantity to five is different from incrementing it by five.
Choose status codes deliberately
Use 201 for a newly created resource and include its location when applicable. A 202 response means work was accepted, not completed; give the client a way to inspect the eventual result. A 204 has no response body. Distinguish unauthenticated requests, usually 401 with an appropriate challenge, from authenticated-but-forbidden requests, usually 403. Some systems deliberately return 404 to conceal resource existence.
Invalid JSON, unsupported media types, validation failures, conflicts, and temporary service failures are different conditions. A 409 can describe a state conflict; a 415 can reject a content type the endpoint does not accept. Pick a consistent 400/422 policy for input validation and document it. Do not retry every 4xx response: correcting the request is usually necessary.
Validate the representation boundary
Treat Content-Type as a declared format, not proof that the body is valid. Bound the body size before parsing; parse JSON once; validate its shape; then apply authorization to the requested operation. TypeScript types on the client do not validate server responses at runtime.
const response = { status: 201, body: { id: "order-42" } };
const accepted = response.status >= 200 && response.status < 300;
console.log({ accepted, orderId: response.body.id });
This small example checks transport classification only. In real code, verify the body contract and handle a response whose body is absent. Test an empty response, malformed JSON, a validation error, a forbidden resource, and an interrupted connection. These cases reveal contract assumptions much earlier than testing only a successful request.