Learn REST API in a Single Post: Complete Tutorial From HTTP Requests to Production API Design

REST is the architectural style behind almost every public API on the web — GitHub, Stripe, Twitter, AWS. It is a set of conventions layered on HTTP that turns the protocol’s request/response model into a predictable, cacheable, stateless interface for manipulating resources. Whether you are building a backend, integrating a third-party service, or designing a public API, REST is the default. This single post teaches the whole subject in five stages, with runnable snippets and five diagrams.

Learning Roadmap

HTTP/REST API Learning Roadmap

The roadmap moves from how HTTP itself works (Stage 1), to REST’s design constraints (Stage 2), to the verb vocabulary (Stage 3), to communicating outcomes (Stage 4), to the production concerns that separate a toy API from a real one (Stage 5).


Stage 1 — HTTP Fundamentals

The request/response cycle

HTTP is a client-server protocol: the client sends a request, the server returns a response. Every interaction is one round trip.

The HTTP Request/Response Cycle

A request has four parts:

POST /users HTTP/1.1          # method + path + version
Host: api.example.com          # headers
Content-Type: application/json
Authorization: Bearer eyJhb...

{"name": "Ada", "email": "ada@example.com"}    # body

A response has three:

HTTP/1.1 201 Created            # version + status code + reason
Location: /users/42              # headers
Content-Type: application/json

{"id": 42, "name": "Ada"}        # body

Methods, headers, URLs

  • Method — the verb: GET, POST, PUT, PATCH, DELETE, plus HEAD, OPTIONS.
  • URL — scheme (https), host, path (/users/42), optional query string (?page=2&limit=20).
  • Headers — metadata: Content-Type (what the body is), Accept (what the client wants back), Authorization (credentials), Cache-Control, custom headers.
  • Body — the payload, usually JSON today (historically XML, form-encoded).

Pitfall: HTTPS is non-negotiable. Plain HTTP exposes auth tokens and request bodies to anyone on the path. Redirect HTTP→HTTPS at the edge and set Strict-Transport-Security.


Stage 2 — REST Principles

REST (Representational State Transfer) is a set of constraints Fielding defined in his dissertation. The practical ones:

Resources and URIs

A resource is anything you want to expose — a user, an order, a collection. URIs name resources as nouns, not verbs:

GOOD (nouns):   /users          /users/42         /users/42/orders
BAD (verbs):    /getUsers       /createOrder      /deleteUser/42

The verb is the HTTP method; the URI is the noun. POST /users (create), GET /users/42 (read), DELETE /users/42 (delete) — the action lives in the method.

Resource modeling and naming

  • Use plural nouns for collections: /users, /orders.
  • Nest to express sub-resources: /users/42/orders (orders belonging to user 42).
  • Use path parameters for identifiers (/users/:id) and query parameters for filtering/sorting/pagination (/users?role=admin&sort=-created_at&page=2).
  • Keep URIs lowercase, hyphen-separated (/order-items, not /orderItems or /order_items — pick a convention and be consistent).
  • Don’t put actions in the URL. If you must deviate from CRUD (e.g. “cancel an order”), prefer a sub-resource POST /orders/42/cancel or, cleaner, model state with PATCH /orders/42 {"status":"cancelled"}.

Statelessness

Every request must contain all the context the server needs to fulfill it — auth token, parameters, body. The server holds no session state between requests. This is what makes REST horizontally scalable: any server can handle any request, because nothing about you lives on one specific box.

Pitfall: “Stateless” means no server-side session state — it does NOT mean your API can’t have a database. The database is shared state, fine. What’s forbidden is server.session["cart"] that only exists on the server that started your conversation. Put the cart in the database (or the client sends it each request).

Uniform interface

Every resource is manipulated the same way: the same methods, the same representation formats (JSON), and responses include enough metadata (links, content types) to act on the resource. This is the constraint that makes REST APIs learnable once and reusable everywhere.


Stage 3 — Methods + CRUD

The five core methods map onto CRUD (Create/Read/Update/Delete):

HTTP Methods Map to CRUD

Method Example Operation Idempotent? Safe?
POST POST /users Create No No
GET GET /users/42 Read Yes Yes
PUT PUT /users/42 Update (full replace) Yes No
PATCH PATCH /users/42 Update (partial) No No
DELETE DELETE /users/42 Delete Yes No

Idempotency and safety — the two concepts that matter most

  • Safe — the method doesn’t modify server state. GET and HEAD are safe. Safe methods can be cached and prefetched freely. (A GET that logs or increments a counter is technically unsafe; keep GET side-effect-free.)
  • Idempotent — repeating the call produces the same result as calling once. GET, PUT, DELETE are idempotent; POST and PATCH are not.

Why this matters: retries. If a network hiccup makes a client unsure whether its DELETE /users/42 arrived, it can safely re-send — the second delete is a no-op (idempotent). But a retried POST /orders creates a second order. That’s why payment APIs issue idempotency keys (see Stage 4).

PUT vs PATCH

  • PUT replaces the entire resource with the body you send. Missing fields become their defaults (or null). PUT /users/42 {"name":"Ada"} wipes out email, role, etc.
  • PATCH applies a partial update. PATCH /users/42 {"name":"Ada"} changes only the name.

Pitfall: Treating PUT like PATCH (sending a partial body to PUT) silently nulls out the fields you didn’t send. Use PATCH for partial updates, PUT for full replacements, and document which your API expects.

A worked CRUD example

# create
curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada","email":"ada@example.com"}'
# -> 201 Created, Location: /users/42, body: {"id":42,...}

# read
curl https://api.example.com/users/42          # -> 200 OK
curl https://api.example.com/users?role=admin   # -> 200 OK, [ ... ]

# full replace
curl -X PUT https://api.example.com/users/42 \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada Lovelace","email":"ada@example.com","role":"admin"}'
# -> 200 OK

# partial update
curl -X PATCH https://api.example.com/users/42 \
  -H "Content-Type: application/json" \
  -d '{"role":"admin"}'
# -> 200 OK

# delete
curl -X DELETE https://api.example.com/users/42  # -> 204 No Content

Stage 4 — Status Codes + Errors

The status code is the server’s answer to “did it work?” Use them deliberately.

HTTP Status Code Classes

The classes

  • 1xx Informational — rare in REST; 100 Continue for large uploads.
  • 2xx Success200 OK (generic), 201 Created (with a Location header), 204 No Content (success, empty body — perfect for DELETE), 202 Accepted (async job started).
  • 3xx Redirection301 Moved Permanently, 304 Not Modified (used with ETag/If-None-Match caching).
  • 4xx Client Error — the client did something wrong: 400 Bad Request (malformed), 401 Unauthorized (no/invalid auth), 403 Forbidden (authed but not allowed), 404 Not Found, 409 Conflict (duplicate), 422 Unprocessable Entity (well-formed but semantically invalid — use this over 400 for validation failures), 429 Too Many Requests (rate limited).
  • 5xx Server Error — the server’s fault: 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout.

The codes you’ll use 90% of the time

Code When
200 OK Successful GET, PUT, PATCH
201 Created Successful POST that created a resource (+ Location header)
204 No Content Successful DELETE, or PUT/PATCH that returns nothing
400 Bad Request Malformed JSON / missing required field in the structure
401 Unauthorized Missing or invalid auth token
403 Forbidden Authed, but not allowed to do this
404 Not Found Resource doesn’t exist
422 Unprocessable Entity Syntactically valid but fails validation (email format, etc.)
429 Too Many Requests Rate limit hit
500 Internal Server Error Unhandled server error

Pitfall: Don’t return 200 OK with {"error": "..."} in the body. That breaks every HTTP client’s error handling — response.ok is false only for non-2xx. Use a 4xx/5xx status and an error body. And use 422 (not 400) when the JSON is valid but a field fails business validation — 400 means “I couldn’t even parse this.”

Structured error bodies: problem+json

RFC 7807 defines a standard error format so clients can parse errors uniformly:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/validation",
  "title": "Invalid request",
  "status": 422,
  "detail": "email must be a valid address",
  "instance": "/users",
  "errors": [
    { "field": "email", "code": "invalid_format" }
  ]
}

A consistent error shape (type, title, detail, field-level errors) lets clients show the right message and highlight the right form field without bespoke parsing per endpoint.

Idempotency keys for non-idempotent operations

For POST operations that must not be duplicated (payments, order creation), the client sends an Idempotency-Key header; the server records the key→response mapping and returns the cached response on a retry:

curl -X POST https://api.example.com/charges \
  -H "Idempotency-Key: 7c8d2f1a-..." \
  -H "Content-Type: application/json" \
  -d '{"amount":5000,"currency":"usd"}'
# a retry with the SAME key returns the original charge, not a new one

Stripe popularized this; it’s the correct fix for “did my payment go through?” after a network timeout.


Stage 5 — Production Design

Authentication

  • Bearer tokens (JWT)Authorization: Bearer <token>. Stateless, self-contained; the server verifies the signature.
  • OAuth 2.0 — for delegated/third-party access (let an app act on a user’s behalf without seeing their password).
  • API keysX-API-Key: <key>; simplest, fine for server-to-server.
  • mTLS — mutual TLS for high-assurance service-to-service.

Store tokens securely (httpOnly cookies for web apps, secure storage for mobile), use short-lived access tokens + long-lived refresh tokens, and never put secrets in URLs (they leak into logs and Referer headers).

Rate limiting

Protect the API from abuse and noisy neighbors. Return 429 Too Many Requests with headers telling the client when to retry:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1690000000
Retry-After: 30

Common algorithms: fixed window, sliding window, token bucket (allows bursts). Use a fast store (Redis) to count per-key.

Versioning

Version from day one so you can evolve without breaking clients. The common schemes:

Scheme Example Notes
URI path /v1/users most common, clearest, cache-friendly
Header Accept: application/vnd.example.v1+json clean URLs, harder to test in a browser
Query /users?version=1 discouraged — breaks caching

Bump the major version on breaking changes (renamed fields, removed fields, changed semantics). Additive, backward-compatible changes (new optional fields, new endpoints) don’t need a new version.

Pagination

Never return an unbounded collection. Two schemes:

  • Offset/limit?page=2&limit=20 (or ?offset=20&limit=20). Simple, but slow for large offsets and unstable if data changes between pages (items shift).
  • Cursor?cursor=eyJpZCI6MTAwfQ&limit=20, where the cursor encodes the last seen position. Stable under insertion, fast (indexed), but no random access.

Return pagination metadata in the response body or Link headers:

Link: <https://api.example.com/users?cursor=abc&limit=20>; rel="next"

Pitfall: Offset pagination on a growing table returns duplicates or skips items as new rows are inserted between page fetches. Use cursor pagination for anything that can change while paginating.

Caching with ETags and conditional requests

GET responses can include an ETag (a hash/version of the representation). On the next request, the client sends If-None-Match: "<etag>"; if the resource hasn’t changed, the server returns 304 Not Modified with an empty body — saving bandwidth and compute.

# first request
HTTP/1.1 200 OK
ETag: "33a64df5"
<full body>

# subsequent request
GET /users/42
If-None-Match: "33a64df5"

HTTP/1.1 304 Not Modified        # body omitted, client uses cached copy

Cache-Control headers (max-age, public, private, no-store) tell caches (CDNs, browsers) how long to hold the response. Mark auth-personalized responses private so shared caches don’t leak them.

OpenAPI / documentation

Generate a spec (openapi.yaml) so your API is discoverable, types are explicit, and clients can be auto-generated. Tools: FastAPI (generates OpenAPI automatically), swagger-codegen, openapi-generator. A /docs endpoint serving Swagger UI lets consumers try endpoints in the browser.

Observability

  • Structured logs with a request ID (return it in a X-Request-ID header so users can quote it).
  • Metrics — request count, latency percentiles (p50/p95/p99), error rate per route.
  • Tracing — distributed tracing (OpenTelemetry) to follow a request across services.

Layered architecture

A production REST API sits in layers — clients hit an edge (load balancer, CDN, API gateway) that fronts services, which front data stores:

Layered REST Architecture

The layered constraint means a client can’t tell (and shouldn’t care) whether it’s talking to the origin server or an intermediary — caching and proxying happen transparently at the edge.


Quick-Start Checklist

  1. Learn the 5 methodsGET POST PUT PATCH DELETE — and their CRUD mapping.
  2. Memorize idempotency and safety — it determines retry behavior.
  3. Use nouns in URIs, verbs in methodsPOST /users, not /createUser.
  4. Return the right status code201 on create, 204 on delete, 422 on validation failure.
  5. Use problem+json for structured, consistent error bodies.
  6. Add idempotency keys to any POST that must not duplicate (payments).
  7. Version from day one/v1/... — so you can evolve.
  8. Paginate every collection — cursor pagination for mutable data.
  9. Add ETags + Cache-Control to cacheable GET responses.
  10. Generate an OpenAPI spec so the API is self-documenting.

Common Pitfalls

  • Verbs in URIs/getUser, /createOrder is RPC, not REST. Use methods + noun URIs.
  • 200 OK with an error body — breaks client error handling. Use 4xx/5xx.
  • 400 for validation errors — use 422 Unprocessable Entity when the JSON parses but fails business rules.
  • Treating PUT as partialPUT replaces the whole resource; missing fields get nulled. Use PATCH for partials.
  • GET with side effectsGET must be safe (no mutations); crawlers, prefetch, and caches will fire it.
  • Retrying POST without an idempotency key — duplicates payments/orders.
  • Secrets in URLs?api_key=... leaks into logs and Referer. Use headers.
  • Unbounded collections — returning 100k items in one response. Always paginate.
  • Offset pagination on mutable data — duplicates/skips as data changes. Use cursors.
  • No versioning — a field rename breaks every client with no escape hatch.

Further Reading

REST is the backbone of the backend stack — these adjacent PyShine tutorials complete it:


REST is small in concept — resources named by nouns, manipulated by methods, with status codes reporting outcomes — but deep in practice. The five stages here take you from “I can read an HTTP request” to “I can design a versioned, paginated, cached, idempotent API with structured errors and an OpenAPI spec.” The skill that separates a junior API designer from a senior one is not knowing more methods; it is honoring the constraints — statelessness, idempotency, the right status code, the right error format — because those constraints are what let REST APIs scale, cache, and survive clients you’ve never met. Implement one CRUD resource end-to-end against a real database, return correct status codes for every edge case, and generate its OpenAPI spec; that one exercise covers most of what matters.

Watch PyShine on YouTube

Contents