Skip to content

REST HTTP Fundamentals

First PublishedLast UpdatedByAtif Alam

This page is intentionally about the contract surface of HTTP APIs: URLs, methods, status codes, error shapes, validation, and idempotency.

For operational guidance (security, observability, performance, retries, rollout checks), use API best practices. For full JSON examples per verb, use Payloads and responses.

A single HTTP call moves through several layers. Names vary by framework, but the idea is stable:

  1. Client builds a request (method, path, headers, optional body).
  2. DNS and TLS resolve the host and encrypt bytes on the wire.
  3. Gateway / ingress may terminate TLS, enforce auth, rate limits, or routing rules.
  4. Router matches path + method to a handler (controller).
  5. Validation checks shape and types (DTO / schema) at the boundary — never trust raw client input deeper in the stack.
  6. Domain / service applies business rules and talks to repositories or other services.
  7. Persistence stores durable state; the response travels back with status, headers, and optional JSON body.

See HTTP for operators for transport-level detail (keep-alive, redirects, common headers).

flowchart TD
  client[Client]
  dnsTls["DNS and TLS"]
  gateway["Gateway / ingress"]
  router[Router]
  handler[Handler]
  validate["Validate DTO"]
  domain["Domain / service"]
  store[Persistence]
  client --> dnsTls --> gateway --> router --> handler --> validate --> domain --> store

Below is a smallest useful exchange: a client asks for one resource; the server answers with 200 and a small JSON object. Real APIs often wrap payloads in a data envelope — that pattern is documented on the payloads page.

GET /v1/users/01933f8a-7d4e-7c9a-b4e1-1c2d3e4f5a6b HTTP/1.1
Host: api.example.com
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{"id":"01933f8a-7d4e-7c9a-b4e1-1c2d3e4f5a6b","email":"[email protected]","name":"Atif"}

api.example.com is only a placeholder; substitute your API base URL. From a terminal, curl is enough to reproduce the GET and see status plus headers (-i):

Terminal window
curl -i --get \
"https://api.example.com/v1/users/01933f8a-7d4e-7c9a-b4e1-1c2d3e4f5a6b" \
-H "Accept: application/json"

HTTPie is another open-source CLI option with readable output. For a GUI, open-source clients such as Insomnia or Bruno work well for the same method, URL, and headers.

JSON request bodies should use Content-Type: application/json. If the client sends the wrong or missing type for a JSON body, servers often respond with 415 Unsupported Media Type.

  • Nouns, not verbs — Prefer /users/123/orders over /getUserOrders.
  • Plural collections/users, /orders.
  • Shallow nesting — Keep hierarchy to one or two levels; deep trees become brittle and hard to cache.
  • Lowercase, hyphenated pathskebab-case in URLs; reserve snake_case for JSON fields if that is your JSON convention.
  • Version in the path — For example /v1/...; simple and explicit for many teams.
  • GET — Safe and idempotent; no body; reads state.
  • POST — Create or non-idempotent actions; not assumed safe to repeat without an Idempotency-Key.
  • PUT — Full replace; idempotent with stable client-provided identity when used that way.
  • PATCH — Partial update; document merge vs JSON Patch semantics.
  • DELETE — Idempotent at the HTTP level (repeat delete remains non-harmful).

On 201 Created, return a Location header for the new resource and usually return the created representation so clients can skip a follow-up GET.

Both layers matter:

  • HTTP status tells generic clients and intermediaries what happened (404, 422, 429, 5xx). Proxies, caches, and retries key off this code.
  • JSON error envelope (for example { "error": { "code", "message", "details", "trace_id" } }) gives humans and app-specific clients structured detail.

A validation failure is still 422 even when the body includes field-level errors. A rate limit is still 429 even when the JSON explains limits. Map domain failures to HTTP once (middleware or central render layer), not ad hoc per handler.

Use a consistent common set:

  • 2xx200 OK, 201 created, 204 no content (common for DELETE).
  • 4xx400 bad input, 401 not authenticated, 403 not authorized, 404 missing, 409 conflict, 412 precondition failed (ETag), 422 validation, 429 rate limited.
  • 5xx — server or dependency failure; clients may retry only when the operation is idempotent or keyed.

Include a correlation or trace ID on every response (header and/or inside the error envelope) and propagate it through logs and downstream calls.

  • Treat OpenAPI (Swagger) as the contract — generate docs, mocks, and clients where practical.
  • Python: FastAPI + Pydantic gives validation, serialization, and OpenAPI from types (layout reference).
  • Go: chi or Gin with validator tags, or spec-first flow with oapi-codegen (layout reference).
  • Validate at the boundary — parse and reject bad input before domain logic runs.
  • ETag + If-Match on PUT/PATCH for optimistic concurrency; respond 412 when the precondition fails.
  • Idempotency-Key header on POSTs that create side effects or charge money — store (key, principal) → response for a bounded TTL (for example 24h) so retries are safe behind load balancers.
  • Resource naming is noun-based, stable, and versioned.
  • Method semantics match behavior (GET safe, PUT/DELETE idempotent by contract).
  • Status code mapping is consistent across endpoints.
  • Error envelope shape is stable and documented.
  • Input validation happens at boundary parsing.
  • Idempotency or precondition strategy is explicit for writes.
  • Trace or correlation ID is included in responses.