ssprkd.io
Backend Essentials
Lesson 1 of 6

HTTP & REST APIs

Design REST APIs that make sense: pick methods by semantics, return the right status codes, and stay stateless with clean versioning and pagination.

Watch

Speed
📖 Read this walkthrough — every command, and why
HTTP & REST APIs — the status code is the contract

Mental model: a web API is a conversation over HTTP. REST gives that conversation a
grammar — your data are nouns you expose as resources (URLs like /todos and /todos/2),
and the HTTP methods are the verbs that act on them. The server's reply is not just a
body: the status code carries the outcome, and the client is meant to read that code
before it reads anything else. Every request/response below is captured verbatim from a
live HTTP server hitting a real /todos resource.

Resources (nouns) and methods (verbs)
    Resource is the thing:        /todos          the collection
                                  /todos/2        one item in it
    Method is the action on it:   GET     read       (safe, doesn't change state)
                                  POST    create     (adds a new resource)
                                  PUT     replace    (full update of an item)
                                  PATCH   modify      (partial update of an item)
                                  DELETE  remove
    You don't name actions in the URL (no /getTodos, no /createTodo). The noun is the URL;
    the verb is the method.

Status code families — the first digit tells you who's responsible
    2xx  success          the request worked
    3xx  redirect / cached go look elsewhere, or you already have it (e.g. 304)
    4xx  client error      the caller got it wrong (bad input, missing resource, no auth)
    5xx  server error      the server faulted; the caller's request was fine
    A client that only reads the body and ignores the code is trusting prose over the
    contract. The code is the contract.

1 · Read the collection
    curl -i http://localhost:3000/todos

    GET /todos            -> 200  body=[{"id":1,"title":"write tests","done":false}]

    200 OK. GET reads; the body is the current list as JSON. GET is safe (it changes
    nothing) and idempotent (calling it again returns the same thing, side-effect-free).

2 · Create an item — 201 Created + Location
    curl -i -X POST http://localhost:3000/todos \
         -H 'Content-Type: application/json' \
         -d '{"title":"ship it"}'

    POST /todos (valid)   -> 201 Created  Location=/todos/2  body={"id":2,"title":"ship it","done":false}

    Not 200 — 201 Created, the code that says a new resource now exists. The Location
    header points at where it lives: /todos/2. That's the creation idiom: 201 + Location.
    The client didn't choose the id 2; the server assigned it and told the client where
    to find it. Note the Content-Type: application/json request header — that's how the
    server knows the body is JSON, not a form or plain text.

3 · Reject bad input — 400 (the client's fault)
    curl -i -X POST http://localhost:3000/todos \
         -H 'Content-Type: application/json' \
         -d '{}'

    POST /todos (no title)-> 400 Bad Request  body={"error":"title is required"}

    Empty body, no title. 400 Bad Request — a 4xx, so the caller got it wrong. The body
    explains ({"error":"title is required"}) but the status alone already tells an
    automated client this is a validation failure, not a server bug and not a missing
    resource. Don't retry a 400 unchanged; the same input fails the same way.

4 · Read the created item — it's really there
    curl -i http://localhost:3000/todos/2

    GET /todos/2          -> 200  body={"id":2,"title":"ship it","done":false}

    200 with the exact todo we created. The write in step 2 stuck: a later, independent
    GET sees it. HTTP is stateless — each request carries its own context and the server
    remembers nothing about "you" between calls — but your data persist behind the API, so
    write-then-read works across separate requests.

5 · Ask for something that was never created — 404
    curl -i http://localhost:3000/todos/999

    GET /todos/999        -> 404 Not Found  body={"error":"not found"}

    Same URL shape as /todos/2, different outcome, different code. 404 Not Found means the
    resource doesn't exist. This is the key distinction learners blur: 400 = you sent bad
    input; 404 = your input was fine but there's nothing at that address. The number tells
    the client exactly what happened without parsing the body.

Idempotency — safe to retry?
    A method is idempotent if making the same call N times leaves the server in the same
    state as making it once. This matters because networks drop responses and clients retry.
        GET     idempotent   read; retrying is harmless
        PUT     idempotent   "set item 2 to this" — same result every time
        DELETE  idempotent   deleting an already-deleted item still ends "gone"
        POST    NOT idempotent — each POST /todos creates a new row. Retry a POST whose
                response you never got and you may create a duplicate (id=3, id=4...).
    That's exactly why creation returns 201 + Location: the server owns the identity, and
    if you want safe-to-retry creates you design around it (idempotency keys, or PUT to a
    client-chosen id).

Statelessness
    The server keeps no per-client session between requests. Everything it needs — the
    target resource in the URL, the method, headers like Content-Type, the JSON body, and
    (for protected routes) an auth token — travels with each request. Persistence lives in
    the datastore behind the API, not in server memory tied to a connection. This is what
    lets any server instance handle any request and lets the system scale horizontally.

JSON bodies + content type
    Both sides agree to speak JSON. Requests that carry a body declare it with
    Content-Type: application/json (step 2), and responses come back as JSON too. The
    body is the payload; the status code and headers are the envelope that says how to
    interpret it.

Notes
  - The status code is the contract. 200 read ok, 201 created (with Location), 400 your
    input, 404 no such resource. Automated clients branch on the code, not the prose.
  - 201 vs 200 on create: use 201 so the caller knows a resource was born, and include
    Location so they can fetch it without guessing the id.
  - 400 vs 404 is the most-missed distinction: bad input (400) is the client's request
    being malformed; missing resource (404) is a well-formed request for something that
    isn't there. Returning 404 for bad input (or 400 for a missing id) breaks the contract.
  - GET/PUT/DELETE are idempotent; POST is not. Only POST creates duplicates on blind
    retry — design creates with that in mind.
  - GET should be safe: never mutate state on a GET. Caches, crawlers, and prefetchers
    assume GET has no side effects.
  - Versioning: once other people depend on your JSON shape, changing it breaks them.
    Version the API up front — a URL prefix like /v1/todos or an Accept header — so you can
    ship /v2 without breaking /v1 callers.
  - Pagination: GET /todos returned two rows here, but a real collection can be millions.
    Don't return everything — page it (e.g. GET /todos?limit=20&offset=40, or cursor-based
    with a next token) and let the client walk the collection.

Verify
    # Round-trip the real contract in five requests. Watch the status line each time.
    curl -i http://localhost:3000/todos                                   # 200, list
    curl -i -X POST http://localhost:3000/todos \
         -H 'Content-Type: application/json' -d '{"title":"ship it"}'     # 201, Location: /todos/2
    curl -i -X POST http://localhost:3000/todos \
         -H 'Content-Type: application/json' -d '{}'                      # 400, title is required
    curl -i http://localhost:3000/todos/2                                 # 200, the created item
    curl -i http://localhost:3000/todos/999                               # 404, not found
    # Pass check: the codes read 200, 201, 400, 200, 404 — and the 201 carries Location: /todos/2.