API design and auth basics
What should the contract between the app and the server look like?
Why did two riders arrive for one lunch?
The kitchen now delivers to the far side of Pune through a bike-courier company. The courier’s developer wrote a script that asks the order API for ready orders every minute and sends a rider to each one.
In its first week two riders arrived at the same kitchen for the same box. The next day nobody came for one student’s lunch at all; he waited until two o’clock before giving up.
The script fetched /orders?page=1, then /orders?page=2, fifty orders a page, newest first. Between page one and page two, new orders arrive at the top and everything slides down, so page two starts with orders that page one already returned. The script sent a rider for each copy, which is how two riders came for one box. The missed lunch is the same bug in reverse: a customer cancelled between the two calls, everything slid up by one, and that order fell into the gap between the pages.
Work out how often this happens before reading on. Orders arrive at about 13 a minute before lunch, and the script takes about ten seconds between pages.
Reading the script turned up something worse. It logged in with the admin password of the person who built the ordering site. The password that could change prices and read every kitchen’s earnings was now in a file on a courier company’s laptop. If that laptop is lost, whoever finds it can change the prices.
Both bugs come from the same gap. Nobody had written down what the kitchen promises a stranger who works with its data, and what it does not.
Pause and predict The courier’s script asks for fifty orders a page, newest first, by page number. What stops orders sliding between pages?
An API that copied the code
The first API grew out of the web pages. Each page needed some data, so each got an endpoint: /getOrdersPage, /updateStuff, /adminLogin. It worked for the web app it was built with. A developer from another company had no way to guess what each endpoint did, what it returned, or how the endpoints related to each other.
Good APIs start from the resources, the nouns of the business, before any endpoint. Google’s API design guide (AIP-121) puts it in that order: identify the resources, then how they relate, then their fields, and only then the methods, preferring a small set of standard ones.
- Resource
- A thing in your business that clients can name and act on, with a stable address.
- For example: An order:
/v1/orders/2187. A kitchen’s menu:/v1/kitchens/k42/menu. A pickup of an order:/v1/orders/2187/pickups.
Then the methods. HTTP gives each method a meaning (RFC 9110), and two properties of those meanings matter for every client that retries:
| Method | Use | Safe (no change) | Idempotent (repeat = same result) |
|---|---|---|---|
| GET | read a resource or a list | yes | yes |
| PUT | replace a resource | no | yes |
| DELETE | remove a resource | no | yes |
| POST | create, or any other action | no | no |
| PATCH | change some fields | no | not guaranteed |
Idempotent means doing it twice leaves the server in the same state as doing it once.
Pause and predict The courier’s script times out waiting for an answer and sends the same request again. Which retry can leave two things behind?
Answers come back with a status code, and a handful carry most of the meaning:
| Code | Meaning | Example |
|---|---|---|
| 200 / 201 / 204 | OK / created / done, nothing to return | list read / pickup created / pickup cancelled |
| 400 / 422 | malformed request / well-formed but invalid | bad JSON / page_size of 5,000 |
| 401 / 403 | not authenticated / not allowed | missing key / courier tries to change a price |
| 404 / 409 | no such resource / conflicts with current state | unknown order / order already picked up |
| 429 | too many requests | see Design a rate limiter |
| 500 / 503 | our bug / temporarily unavailable, try later | a crash / overloaded or in maintenance |
The 4xx codes say “the client should change something”; 5xx say “the server failed, retrying may help”. That difference tells a script whether to retry.
A mental model
- Cursor pagination
- Each page comes back with a token that marks where it ended. The next request sends the token back and gets the items after that point.
- For example:
GET /v1/orders?page_size=50→ 50 orders andnext_page_token.GET /v1/orders?page_size=50&page_token=…→ the next 50.
How often did it go wrong?
Most of the 2,000 daily boxes are ordered in the 90 minutes before lunch. 1,200 orders in 90 minutes is about 13 a minute.
The script fetched page 2 about ten seconds after page 1. In ten seconds about 13 x 10 / 60 ≈ 2 new orders arrive, so page 2 started with about 2 orders that page 1 already had. Each of those orders got two riders.
Each cancellation in those ten seconds does the opposite. The list shifts up by one, so one order moves from the top of page 2 to the bottom of page 1, which the script has already read. Nobody sees that order until a later poll, and only if that poll’s pages happen to catch it.
Now count what it costs to check the script’s credentials, which matters for the trade-off below. The script polls once a minute: 60 x 24 = 1,440 requests a day. If checking a key means one indexed lookup of about 1 ms, the whole day’s checks cost about 1.4 seconds of database time.
How should a partner prove who it is?
The courier’s script needs its own identity, separate from any person, limited to what a courier does (read ready orders, create pickups), and revocable. That much is settled. The argument is about the token, and it is between two goals: no lookup per request, and a leaked credential that dies at once.
- JWT (JSON Web Token)
- A token that carries its own claims (who, what scopes, when it expires) plus a signature. The server checks the signature with a key and trusts the claims without looking anything up.
- For example:
header.payload.signature, each part base64url-encoded. The payload{"sub":"courier-1","scope":"orders.read","exp":…}is readable by anyone who holds the token.
The first argument is real at large scale: a lookup on every request, for millions of requests a second, needs its own fast store. At 1,440 requests a day it is nothing, and cutting off a leaked key at once is the requirement that protects the prices.
Customers in the app keep logging in as before. Their browser holds a session cookie (marked HttpOnly, Secure and SameSite), which points to their session in the shared store from lesson 1.1. People log in and get a session; programs get their own keys.
What to remember
- Design the resources first (nouns a client needs), then the methods. Do not copy your tables or your web pages into the API.
- GET, PUT and DELETE are idempotent; POST is not. That decides what a client may safely retry.
- Status codes are part of the contract: 4xx means the client must change something, 5xx means the server failed and a retry may help.
- Page with a cursor over a unique, stable order, not an offset: offsets repeat and skip items when the list changes.
- People log in with sessions; programs hold their own scoped, revocable keys. A JWT is readable and hard to revoke early.
Page by cursor, never by position, and give every program its own key.
In the real world
Our toy API has one partner and four endpoints. Large public APIs add more of the same ideas: OAuth 2.0 so that customers can grant a third party limited access to their own data, rate limits per key (lesson 1.6), idempotency keys on every POST (Part 4), dated versions pinned per account (Stripe’s approach), and generated client libraries from a written specification such as OpenAPI.
A tool an agent calls is an API with a very literal client: it reads your names and error codes, and it retries exactly as far as you let it. Tools and agents in LLM From First Principles.