api design, one rule per page
Thirty rules for developers who ship endpoints but were never handed the contract underneath: the two envelopes every request and response really are, why safe and idempotent are the words that decide what crawlers and retries do to your data, POST as the honest escape hatch, PUT that replaces rather than merges, the five shelves of status codes from 401 asking for ID to 409 naming a conflict, redirects that keep the verb, Retry-After calming the stampede, and the OpenAPI file that turns one contract into docs, clients and mocks.
A diagram, the trap, and one thing to go fix this week. That's a page.
api design, one rule per page
Thirty rules for developers who ship endpoints but were never handed the contract underneath: the two envelopes every request and response really are, why safe and idempotent are the words that decide what crawlers and retries do to your data, POST as the honest escape hatch, PUT that replaces rather than merges, the five shelves of status codes from 401 asking for ID to 409 naming a conflict, redirects that keep the verb, Retry-After calming the stampede, and the OpenAPI file that turns one contract into docs, clients and mocks.
Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).
HTTP semantics checked against RFC 9110 (HTTP Semantics, June 2022) fetched from rfc-editor.org and read during this build; request-method guidance cross-checked against MDN Web Docs HTTP reference pages (developer.mozilla.org); OpenAPI facts checked against the official OpenAPI Specification 3.1.1 from the OpenAPI Initiative (spec.openapis.org). Status codes, headers, method properties and field names are quoted as those sources state them. Teaching conventions (one rule a page) are named as conventions. An independent guide, not affiliated with or endorsed by the IETF, Mozilla, or the OpenAPI Initiative.
General information only. Not professional advice; verify against the current specifications and your own stack.
© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.
Edition 1.0 · stevehodgkiss.net
Contents
The trade
What a request and a response actually are, and the method properties, safe and idempotent, that decide what the network, crawlers and retries are allowed to do to your data.
- 01A request, a response
- 02Safe means read-only
- 03Idempotent survives
Per RFC 9110 section 6: a request message includes a request line containing a method, a target URI, and the HTTP version; request messages can also contain header fields and content. Section 15: the status code of a response is a three-digit integer code that describes the result of the request.
A request, a response
Let's say you write endpoints all day and have never seen the shape underneath. A request is a method, a target, and the version, then headers, then optional content. That's all a client ever sends.
The server answers with a three-digit status code that, per RFC 9110, describes the result of the request, plus its own headers and content.
Two envelopes. Everything else in this book is about what you write on them.
Open your browser's devtools network tab today and read one request line and one status line out loud.
Per RFC 9110 section 9.2.1 Safe Methods: request methods are considered safe if their defined semantics are essentially read-only; of the request methods defined by this specification, the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe. The purpose of the distinction is to allow spiders and prefetching to work without fear of causing harm.
Safe means read-only
Let's say your delete link is a GET because it was easier to put in an href. Safe, per RFC 9110, means the defined semantics are essentially read-only: GET, HEAD, OPTIONS and TRACE.
The reason the word exists: safe methods let spiders and prefetching work without fear of causing harm. The spec calls out the page?do=delete pattern by name: automated processes will GET every URI for link maintenance and indexing, and the unfortunate side effect is yours.
Anything that changes state, a crawler will eventually trigger. Don't hand it a GET.
Grep your routes for state changes reachable by GET this week. Each one is a bug waiting for a crawler.
Per RFC 9110 section 9.2.2 Idempotent Methods: a request method is considered idempotent if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request. PUT, DELETE, and safe request methods are idempotent. Idempotent requests can be repeated automatically if a communication failure occurs before the client reads the response.
Idempotent survives
Let's say the connection dies after you sent the request but before you read the response. For idempotent methods the question doesn't hurt: multiple identical requests have the same intended effect as one. PUT, DELETE, and the safe methods qualify.
So the client can repeat the request on a new connection. POST can't: RFC 9110 says a proxy MUST NOT automatically retry non-idempotent requests, since three identical POSTs may mean three orders.
The network will eat a request someday. Design so the retry is free.
List your POST endpoints that a lost response would make dangerous. Those are your idempotency-key candidates.
Status codes
The response's first digit is a contract. The codes that carry the most meaning per character: who erred, what conflicts, what moved, and what the client should do next.
- 01The five shelves
- 02401 asks, 403 refuses
Per RFC 9110 section 15: the status code of a response is a three-digit integer code; all valid codes are 100 to 599. 1xx informational, 2xx successful, 3xx redirection, 4xx client error, 5xx server error. A client MUST understand the class of any status code and treat an unrecognized code as the x00 code of that class; values outside 100..599 are invalid and SHOULD be processed as 5xx.
The five shelves
Let's say your API invents status 437 and a client chokes. The spec already planned for that: a client must understand the class of any code and treat an unrecognized one as the x00 of its class. A mystery 471 reads as a 400.
The five shelves: 1xx the request was received, 2xx received, understood, accepted, 3xx further action needed, 4xx the client erred, 5xx the server failed a valid request.
The first digit is the contract. The last two are your vocabulary.
Skim your API's response codes today and confirm every one sits on the shelf whose meaning it actually has.
Per RFC 9110 section 15.5.2: the 401 response MUST send a WWW-Authenticate header field containing at least one challenge. Section 15.5.4: 403 means the server understood the request but refuses to fulfill it; the client SHOULD NOT automatically repeat the request with the same credentials. MDN notes 401 semantically means unauthenticated.
401 asks, 403 refuses
Let's say your API answers 401 for every denial. The two codes split the question. 401 means unauthenticated: the server doesn't know who you are, and the response must carry a WWW-Authenticate challenge.
403 means the server understood and refuses: it knows you, and your credentials are not enough. The spec adds the discipline: the client should not repeat the request with the same credentials.
401 is a locked door asking for ID. 403 is ID checked, entry denied.
Audit your auth middleware today: which denials are truly missing credentials, and which are 403s wearing 401?