# HTTP API Architecture

> HTTP as an application protocol: methods, status classes, idempotency, problem details, versioning, pagination and edge behavior for public APIs.

- Canonical: https://thearchitecturedeepdive.blog/articles/http-api-architecture.html
- Series: The Architecture DeepDive, No. 010
- Author: Barnabas Waweru
- Published: 2026-08-26
- Tags: Systems
- Cite as: Barnabas Waweru, "HTTP API Architecture", The Architecture DeepDive, https://thearchitecturedeepdive.blog/articles/http-api-architecture.html

---

Request → Contract

A public HTTP API is not a function with extra headers. It is a contract over a protocol that already has methods, cache rules, and status classes. Ignore those and you reinvent RPC on port 443, then spend years documenting the exceptions.

Barnabas Waweru 26 August 2026 17 min read

## HTTP Is the Architecture

### RFC 9110 is not optional reading

HTTP Semantics (RFC 9110) defines what a method means, when a response is cacheable, and which status class a client should treat as retryable. Your framework's router does not override that. If you POST to create and return 200 with a body that changes on every retry, you have left the protocol and you will pay for it in client libraries.

REST is a set of constraints (Fielding). Most "REST APIs" are resource-oriented HTTP. That is fine. Be honest about which constraints you kept: uniform interface, stateless requests, cacheable responses. If every call is a POST to `/rpc`, say it is RPC.

![An HTTP request as a cyan glass dart piercing layered dark-glass membranes](https://thearchitecturedeepdive.blog/assets/articles/http-api-architecture/lifecycle.jpg)

One request crosses client cache, CDN, origin, and store. Each layer can answer, mutate, or fail. The status code has to mean the same thing at every layer.

1

Client

Method, target, headers, optional body. Idempotency-Key on unsafe POST.

2

Cache / edge

RFC 9111 reuse or miss. Auth and Vary decide whether a shared cache may answer.

3

Handler

Validate, authorize, mutate or read. Status class is the control plane.

4

Response

Resource, 202+queue, or problem+json. Cache-Control belongs here, not in a later middleware guess.

schema · request layers

```
CLIENT
  method + target + headers + optional body
       │
       ▼
BROWSER / APP CACHE     (RFC 9111)
       │
       ▼
CDN / EDGE              (Cache-Control, Vary, auth)
       │
       ▼
ORIGIN GATE             (TLS terminate, WAF, rate limit)
       │
       ▼
APP HANDLER             (authn, authz, validate, mutate)
       │
       ▼
STORE / QUEUE           (commit or enqueue)
       │
       ▼
RESPONSE                status + headers + problem or resource
```

## Methods Are Semantics

| Method | Safe | Idempotent | Use |
| --- | --- | --- | --- |
| GET | Yes | Yes | Read. No side effects. Cacheable when headers allow. |
| HEAD | Yes | Yes | Metadata only. Same headers as GET, empty body. |
| PUT | No | Yes | Replace the resource at this URL with this representation. |
| DELETE | No | Yes | Remove. A second DELETE is still success (404 or 204, pick one and keep it). |
| POST | No | No | Process. Create, trigger, or anything that is not a replacement. |
| PATCH | No | Conditional | Partial update (RFC 5789). Idempotent only if your patch language is. |

POST is the escape hatch, not the default

Use POST when the protocol has no safer verb: creating a resource whose URL the server assigns, kicking a workflow, or accepting a command. If you can PUT a known URL, do that. Clients and caches understand PUT. They cannot infer anything from a zoo of POST paths.

## Status Classes

2xx Success

201 with Location on create. 202 when the work is queued. 204 when there is no body. 200 is not a universal solvent.

3xx Redirect

304 for conditional GET. 301/308 change the permanent URL. Do not 302 a POST unless you want a confused client.

4xx Client

400 validation. 401 unauthenticated. 403 authenticated but forbidden. 404 unknown. 409 conflict. 412 precondition. 429 rate limit.

5xx Server

500 unexpected. 502/504 upstream. 503 overload with Retry-After. Clients may retry 5xx. Make them safe to retry.

### Do not collapse the classes

Returning 200 with `{"error": true}` trains every client to parse bodies for control flow. Load balancers, caches, and synthetic checks look at the status line. If the status lies, the rest of the stack lies with it.

## Idempotency

![Two identical request darts hitting an idempotency gate that emits one commit](https://thearchitecturedeepdive.blog/assets/articles/http-api-architecture/idempotency.jpg)

Timeouts duplicate requests. The gate has to remember the first commit, not hope the client will not retry.

### The network will retry

Mobile clients, gateways, and your own jobs retry on timeout. A timeout is not a known failure. The request may have committed. POST without an idempotency key double-charges. PUT to a known URL does not, if replace is truly replace.

The usual pattern: client sends `Idempotency-Key` (or `Idempotency-Key` via a documented header). Server stores the key with the request hash and the response. A replay with the same key and body returns the stored response. A replay with the same key and a different body is 409.

TypeScript · idempotency gate

```
type Stored = { hash: string; status: number; body: unknown };

async function withIdempotency(
  key: string,
  bodyHash: string,
  store: Map<string, Stored>,
  work: () => Promise<{ status: number; body: unknown }>
) {
  const hit = store.get(key);
  if (hit) {
    if (hit.hash !== bodyHash) {
      return { status: 409, body: { type: "https://api.example/conflicts/idempotency" } };
    }
    return { status: hit.status, body: hit.body };
  }
  const result = await work();
  store.set(key, { hash: bodyHash, status: result.status, body: result.body });
  return result;
}
```

mermaid · sequence

```
sequenceDiagram
  participant C as Client
  participant G as API gate
  participant D as Store
  C->>G: POST /charges Idempotency-Key=k1
  G->>D: lookup k1
  D-->>G: miss
  G->>D: insert pending + commit charge
  G-->>C: 201 Charge
  C->>G: POST /charges Idempotency-Key=k1 (retry)
  G->>D: lookup k1
  D-->>G: stored 201
  G-->>C: 201 Charge (same body)
```

## Problem Details

### RFC 9457 instead of ad-hoc JSON

Problem Details (RFC 9457, successor to 7807) gives you `type`, `title`, `status`, `detail`, and `instance`. The `type` is a URI your clients can switch on. Human copy lives in `title` and `detail`. Do not invent a third error envelope per team.

JSON · application/problem+json

```
{
  "type": "https://api.example.com/problems/insufficient-funds",
  "title": "Insufficient funds",
  "status": 409,
  "detail": "Account acc_9f cannot cover this charge.",
  "instance": "/charges/ch_01J",
  "account_id": "acc_9f"
}
```

Validation errors are a list, not a string

A 400 that says "invalid body" wastes a round trip. Point at the field. If you need a machine-readable list, add an extension member (RFC 9457 allows it) and document it in the OpenAPI component.

## Contract, Versioning, Pagination

### OpenAPI is the boundary

Generate clients from a spec you publish, or generate the spec from types you own. Do not do neither. Breaking changes are: removing a field, changing a type, changing a status, renaming a path. Adding an optional field is not breaking if clients ignore unknown keys (they must).

Version in the media type (`application/vnd.example.v2+json`) or in the path (`/v2`). Header versioning works if every intermediary forwards it. Many CDNs and logs do not. Path versioning is ugly and survivable.

Cursor pagination

Opaque cursor over a stable sort. Survives inserts. Offset pagination (page=7) drifts under write load.

Conditional GET

ETag + If-None-Match. 304 saves payload. Weak ETags if you only need byte-change detection.

Optimistic concurrency

If-Match on PUT/PATCH. 412 when the resource moved. Better than last-write-wins for editors.

Rate limits

429 + Retry-After. Document the unit (token bucket, fixed window). Do not hide the remaining budget if you can send RateLimit headers (IETF draft / RFC 9331-adjacent practice).

## The Edge Changes Your API

### Cache-Control is part of the handler

RFC 9111 decides whether a GET can be reused. `Cache-Control: private, no-store` on authenticated JSON. `public, s-maxage=60, stale-while-revalidate=120` on a catalog. `Vary: Authorization, Accept-Language` if those change the body. A missing Vary is a cross-user leak waiting for a shared cache.

Authorization belongs in a header or a signed cookie, not in a query string that lands in CDN logs. Prefer bearer tokens that the origin validates. If you terminate auth at the edge, the origin must still not trust an unverified internal header unless the network path is locked.

mermaid · flowchart

```
flowchart TD
  R[Request] --> A{Auth present?}
  A -->|no, public GET| C{Fresh in CDN?}
  C -->|yes| H[200 from cache]
  C -->|no| O[Origin]
  A -->|yes| E[Edge verify or pass through]
  E --> O
  O --> V[Handler]
  V -->|GET cacheable| S[Set Cache-Control + ETag]
  V -->|POST mutate| I[Idempotency + 2xx/4xx]
```

Timeouts belong in the contract

Document the origin time budget. A client that waits 100 seconds on a 10-second origin will retry into a pileup. Return 503 with Retry-After when you shed load. Returning a slow 200 after 55 seconds trains every mobile radio to hold a socket you cannot afford.

## Key Takeaways

### Principles

1.  **Honor RFC 9110 method semantics.** Safe and idempotent are protocol facts, not style advice.
2.  **Let the status class speak.** Do not bury errors in a 200 body.
3.  **POST needs an idempotency key.** Timeouts are retries you did not ask for.
4.  **Use problem+json (RFC 9457).** One error shape for every client.
5.  **Version on purpose.** Additive fields are cheap. Renames are a new version.
6.  **Cursor-paginate write-heavy lists.** Offset pages drift.
7.  **Cache-Control and Vary are security controls.** Shared caches will obey what you send.
8.  **Publish OpenAPI and generate something from it.** A wiki is not a contract.

## Official Documentation

### Primary sources

-   [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)
-   [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111)
-   [RFC 9457: Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457)
-   [RFC 5789: PATCH](https://www.rfc-editor.org/rfc/rfc5789)
-   [OpenAPI Specification](https://spec.openapis.org/oas/latest.html)
-   [Fielding: REST architectural style](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)
-   [HTTP Working Group specifications](https://httpwg.org/specs/)
