What REST actually means
What Fielding's dissertation really says about resources, verbs and statelessness — and how it differs from RPC and GraphQL.
REST — what Fielding actually said · resources, verbs, statelessness · vs RPC vs GraphQL Sockets — the API under every server you will ever write
The sentence to own: REST is an architectural style, not a specification — and almost nothing called a "REST API" is fully RESTful. That is fine. What is not fine is not knowing the difference, because "design me an API" is a real interview round and the vocabulary is the round.
Part 1 — What Fielding actually said
Roy Fielding's 2000 dissertation described the constraints that made the web itself scale. REST is a description of the web's architecture, generalised.
| Constraint | Meaning | What it buys |
|---|---|---|
| Client–server | separate concerns | evolve independently |
| Stateless | every request carries everything needed | any server can handle any request ⇒ horizontal scaling |
| Cacheable | responses say whether they may be cached | fewer requests |
| Uniform interface | resources, representations, standard verbs | one client shape for every server |
| Layered system | a client cannot tell it is talking to a proxy | CDNs, load balancers, gateways all become possible |
| Code on demand (optional) | server can ship code | (this is JavaScript) |
| HATEOAS | responses contain the links to what you can do next | ⚠ almost nobody does this |
THE RICHARDSON MATURITY MODEL — the honest ladder:
Level 0 — one URL, one verb (POST /api), the method is in the body "SOAP"
Level 1 — RESOURCES: /users, /orders nouns appear
Level 2 — HTTP VERBS + STATUS CODES used correctly ← everyone lives HERE
Level 3 — HATEOAS: hypermedia controls in the responses ~nobody
⇒ Say this in an interview: "we build Level 2, and I know it — Level 3's benefit is
decoupling the client from URL structure, and the cost is a complexity almost no team
recovers. GraphQL solved the same discovery problem differently and won."
Statelessness is the constraint that matters most to you, and Day 005 gave its precise meaning: not "no state", but no client state in the server's memory between requests. State lives in the database, in Redis, or in the request itself.
⚠ The Python-specific version of getting this wrong, and you will meet it in Stage 4:
SESSIONS = {} # ⚠️⚠️ a module-level dict of user state
# Works perfectly in development — one process.
# uvicorn --workers 4 ⇒ FOUR PROCESSES, four separate dicts (Day 003: no shared memory).
# ⇒ the user logs in on worker 2 and is logged out on worker 3. Intermittently. Unreproducibly.
# ⇒ FIX: Redis, or a signed cookie. Never process memory. (Day 019.)
Part 2 — Designing the resources
RESOURCES ARE NOUNS. VERBS ARE HTTP METHODS. That is the whole rule.
✗ POST /createUser ✓ POST /users
✗ GET /getUserById?id=5 ✓ GET /users/5
✗ POST /updateUser ✓ PATCH /users/5
✗ POST /deleteUser ✓ DELETE /users/5
✗ GET /users/5/delete ✗✗ a crawler will call this (Day 010)
NESTING — one level, for containment:
GET /users/5/orders orders belonging to user 5
POST /users/5/orders
⚠️ GET /users/5/orders/9/items/3/reviews ⇒ too deep. Use /reviews/… with a filter.
| Concern | Convention |
|---|---|
| Filtering | GET /orders?status=paid&min=100 |
| Sorting | ?sort=-created_at |
| Pagination | ?limit=50&cursor=... — cursor beats offset at scale (Stage 5) |
| Sparse fields | ?fields=id,name |
| Versioning | /v1/... in the path (blunt, obvious, wins in practice) or a header (pure, harder to debug) |
| Errors | one consistent shape: {"detail": ..., "code": ...} — and the right status code |
Two hard-won API rules worth stating in a design round: (1) be liberal in what you accept and conservative in what you emit — adding a field is backwards-compatible, removing or renaming one is not; (2) an API is a promise you cannot take back, because you do not control the clients, which is why versioning exists at all.
⚠ Never return an ORM object directly — it serialises every column, including
password_hash. In FastAPI the guard is response_model=UserOut; Stage 3 devotes a day to this
because it is one of the three most damaging bugs in Python backends.
Part 3 — REST vs RPC vs GraphQL vs gRPC
| REST | RPC / gRPC | GraphQL | |
|---|---|---|---|
| Mental model | resources you act on | functions you call | a query language over a graph |
| Shape | GET /users/5 |
getUser(5) |
{ user(id:5) { name } } |
| Over-fetching | yes — fixed representation | fixed | solved — ask for the fields you want |
| Under-fetching | yes — N calls for N related things | yes | solved — one round trip |
| Caching | free — HTTP does it | manual | ⚠ hard — everything is a POST to /graphql |
| Typing | OpenAPI (bolted on) | protobuf (built in) | schema (built in) |
| Best at | public APIs, CRUD | service-to-service, internal | many clients with different data needs |
THE HONEST SUMMARY:
REST ⇒ the default. Boring, cacheable, debuggable with curl, everyone knows it.
gRPC ⇒ internal service-to-service: binary, typed, fast, streams. (Stage 12.)
GraphQL ⇒ when many different clients need different slices of the same data
⚠️ and you accept: no HTTP caching, N+1 by default, and hard rate limiting
(a single query can ask for the whole database — you need query depth/cost limits).
GraphQL's N+1 problem is the one to be able to name: a query for 100 posts each with an
author naturally issues 1 + 100 database queries, because each field resolver runs independently.
The fix is a DataLoader — batching the per-field lookups into one query per tick of the event
loop (strawberry/graphene both provide one), and it is the same N+1 you will fix in SQLAlchemy
in Stage 5, arriving from a different direction.
Part 4 — C-11 · Sockets — and how asyncio is built
Day 004 you used the socket API. Today: what it is, and how one thread serves thousands.
THE API, in the order you call it:
SERVER: socket() → bind() → listen() → accept() → recv()/send() → close()
CLIENT: socket() → connect() → send()/recv() → close()
A socket is a FILE DESCRIPTOR — an integer index into your process's open-file table.
⇒ that is why "too many open files" is the symptom of leaked sockets (Day 008)
⇒ and why the same read/write calls work on files, pipes and sockets alike.
BLOCKING vs NON-BLOCKING — THE FORK IN THE ROAD:
BLOCKING (Day 004): conn.recv(4096) ⇒ the THREAD STOPS until bytes arrive.
⇒ to serve N clients you need N threads/processes ⇒ memory and context-switch cost
NON-BLOCKING + READINESS: sock.setblocking(False) ⇒ recv() raises instead of waiting.
⇒ so ASK THE KERNEL which of your 10,000 sockets are ready:
select() — O(n) scan, ~1024 fd limit portable, old
poll() — O(n), no fd limit
epoll (Linux) / kqueue (BSD) — O(1), the kernel keeps the ready list
⇒ ONE THREAD, THOUSANDS OF CONNECTIONS. This is the C10K solution.
# THIS IS WHAT asyncio ACTUALLY IS, in ten lines. There is no magic underneath.
import selectors, socket
sel = selectors.DefaultSelector() # picks epoll on Linux, kqueue on macOS
srv = socket.socket(); srv.bind(("", 8000)); srv.listen(); srv.setblocking(False)
sel.register(srv, selectors.EVENT_READ)
while True:
for key, _ in sel.select(): # BLOCKS until ANY registered socket is ready
if key.fileobj is srv:
conn, _ = srv.accept(); conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ)
else:
data = key.fileobj.recv(4096) # guaranteed not to block — it is READY
...
Read that loop next to Day 014's diagram. sel.select() is the event loop's wait;
the ready sockets are the queued callbacks. asyncio is this loop plus coroutines so you can
write the callbacks as straight-line code — and now "why does a blocking call destroy asyncio" has
a mechanical answer: while your code runs, sel.select() is not being called, so nothing else can
be noticed as ready.
Common mistakes
| Mistake | Correction |
|---|---|
Verbs in URLs (/getUser) |
Resources are nouns; HTTP methods are the verbs. |
| Session state in a module-level dict | Multiple workers = multiple processes. Redis or a cookie. |
| Claiming your API is RESTful | It is Level 2. Say so — knowing the ladder is the signal. |
| Returning ORM objects | Leaks every column. Use an explicit response model. |
Deep nesting (/a/1/b/2/c/3) |
One level. Then top-level with filters. |
| Offset pagination at scale | OFFSET 100000 scans 100,000 rows. Cursors. |
| Removing or renaming a response field | Breaking. Adding is safe; that asymmetry is the whole rule. |
| Choosing GraphQL by default | You lose HTTP caching and gain N+1 and rate-limiting problems. |
Thinking asyncio is threads |
It is one thread plus epoll. See the loop above. |
| Leaving sockets blocking in an async design | One blocked recv stops the entire loop. |
Interview questions
Q: What actually makes an API RESTful?
Fielding's constraints, not the URL style. The ones with real consequences are statelessness — any server can serve any request, which is what makes horizontal scaling possible — cacheability, and the layered system, which is why a CDN or a load balancer can sit in the middle and the client can't tell. Full REST also requires hypermedia controls, HATEOAS, and almost nobody implements that. Most APIs, including mine, are Richardson Level 2: resources plus correct verbs and status codes. I'd rather say that accurately than claim Level 3.
Q: What does stateless really mean, and where does it go wrong in Python?
It means the server keeps no per-client state in memory between requests — state lives in the database, in Redis, or in the request itself, usually a signed token. It goes wrong the moment you put sessions in a module-level dict. That works in development because there's one process, and breaks in production because uvicorn or gunicorn runs several worker processes with separate memory — so the user logs in on one worker and appears logged out on the next request. It presents as an intermittent, unreproducible logout, which is the worst kind of bug to chase.
Q: REST or GraphQL?
REST by default: it's cacheable by the HTTP infrastructure I already have, debuggable with curl, and universally understood. GraphQL when I have several clients that need genuinely different slices of the same graph, and mobile bandwidth makes over-fetching expensive. The costs are real though — everything is a POST to one endpoint, so HTTP caching is gone; resolvers give you N+1 by default until you add DataLoader batching; and a single query can be arbitrarily expensive, so you need depth and cost limits before it's exposed publicly.
Q: When would you pick gRPC?
Internal service-to-service traffic. It gives me a typed contract in protobuf, binary framing, streaming, and generated clients — all of which matter far more between my own services than human-readable JSON does. I wouldn't put it on a public API, because browsers can't speak it natively and every consumer would need tooling.
Q: How does one thread serve ten thousand connections?
By never blocking on any of them. The sockets are set non-blocking, and the kernel is asked which ones are ready —
epollon Linux,kqueueon BSD, both O(1) because the kernel maintains the ready list rather than scanning. The loop handles each ready socket, which by definition won't block, then asks again. That's exactly whatselectorsdoes in the standard library, and asyncio is that loop with coroutines layered on so the callbacks read as sequential code.
Q: Given that, why is a blocking call inside async def so bad?
Because while my code is running, the loop isn't calling
select. Nothing can be observed as ready, no other coroutine can be resumed, and every other connection this worker owns is simply frozen until my call returns. It's not a slowdown proportional to the work — it's a full stop for everyone.
Mini task
- Take a bad API —
POST /getUserOrdersand friends — and redesign it as resources. Write out method, path, status codes and error shape for eight endpoints. - Design pagination for
/orderstwice: offset and cursor. Name what breaks at a million rows. - Write the
selectorsloop above and serve two browser tabs at once from one thread — with no threading and noasyncio. - Then add
time.sleep(5)inside the handler of that loop and watch both tabs stall. (This is Day 014 and Day 072, at the syscall level.) - Add
sock.setblocking(True)back and see the difference in behaviour. curla public REST API (GitHub's) and find its pagination, versioning and rate-limit headers.- Open a public GraphQL playground and write one query that would be three REST calls.
Exit questions
Answer aloud, no notes.
- Name Fielding's constraints. Which three have the biggest practical consequences?
- What does stateless mean precisely, and what does it buy?
- Describe the Python bug where statelessness is violated by a dict, and why it only appears in production.
- Give the Richardson levels. Which one do real APIs live at?
- Resource-design rules: nouns, nesting depth, filtering, versioning.
- Which API changes are backwards-compatible and which are not?
- REST vs gRPC vs GraphQL — one sentence each on when.
- What is GraphQL's N+1 problem and its fix?
- What is a socket, in terms of the operating system?
- Blocking vs non-blocking + readiness — what does each cost?
- What do
select,pollandepolldo, and why isepollbetter? - Explain
asyncioin terms ofselectors, then explain the blocking-call bug mechanically.
Articulation drill
Record two minutes: "Design a REST API for a library system."
Resources first — /books, /members, /loans — and note that a loan is a resource, not a
verb, which is the design move that separates people who have done this from people who write
POST /borrowBook. Then the operations: POST /loans to borrow, PATCH /loans/5 with a return
date, GET /members/5/loans for containment. Status codes: 201 with Location, 409 when the
copy is already out, 403 when the member is barred, 404 deliberately for a book they may not see.
Then the sentence that ends the round well: "It is Level 2, not Level 3 — and the constraint I would actually defend is statelessness, because it is what lets me run four workers behind a load balancer without sticky sessions."
Previous: Day 015 · Tomorrow: Day 017 — WebSockets: why polling fails, the upgrade handshake, and frames you will parse by hand