Section 4: The Contract Writers Type Into - API Design (with pushback)
Executive Summary: Section 3 found two workloads with almost nothing in common: an editor that autosaves about 450 times per publish, and a read path the CDN nearly owns. We lay out the whole surface, walk Maya's autosave and publish and Daniel's read, then defend five shapes under pushback: full-document saves, publish as an action, a separate /me, the ID in the URL, and deduplicated claps.
"Show me the API." Put the whole surface on the board first, one line per endpoint, before arguing about any of it.
| Job | Endpoint |
|---|---|
| Start a post | POST /v1/publications/{pubId}/posts → 201 {postId, draftVersion: 0} |
| Load draft | GET /v1/posts/{postId}/draft → {doc, draftVersion, updatedAt, deviceLabel} + ETag |
| Autosave | PUT /v1/posts/{postId}/draft + If-Match → 200 {draftVersion} or 412 |
| Save a copy | POST /v1/posts/{postId}/revisions {doc, clientSaveId, reason: "conflict"} → 201 {revisionId} |
| Publish, schedule | POST /v1/posts/{postId}/publish {draftVersion, publishAt?} → 200 |
| Unpublish | POST /v1/posts/{postId}/unpublish → 200 |
| History, restore | GET /v1/posts/{postId}/revisions, POST /v1/posts/{postId}/revisions/{revisionId}/restore |
| Images | POST /v1/uploads → {uploadUrl, imageId} |
| Read an article | GET /@{handle}/{slug}-{postId} → HTML |
| Personal state | GET /v1/posts/{postId}/me → {claps, following} |
| View beacon | POST /v1/events/views (sendBeacon) → 204 |
| Clap | POST /v1/posts/{postId}/claps {count, clientEventId} → {yourClaps} |
| Follow, unfollow | PUT /v1/me/follows/{pubId}, DELETE /v1/me/follows/{pubId} |
| Lists | GET /v1/me/feed?cursor=, GET /v1/publications/{pubId}/posts?cursor= |
| Search (P1) | GET /v1/search?q=&cursor= |
"Three rules hold everywhere. No body carries an authorId or userId: who you are comes from the session, and every draft call checks that you're a member of the post's publication. {postId} is always the random 12-hex public ID; internal tables, KV keys and Kafka use a 64-bit time-ordered post_id, translated once on a unique index. And the API splits into two halves with opposite caching rules: article HTML is public and cached at the edge, while everything under /me and the editor is Cache-Control: private, no-store and never touches the CDN."
Autosave: The Endpoint That Takes the Load
Maya's editor sends the whole document 2 s after typing pauses, and at least every 10 s during continuous typing (a debounce with a 10 s max-wait), averaging about 3 a minute:
PUT /v1/posts/a81f3c9e2b7d/draft
If-Match: "314"
{ "doc": { "type": "doc", "content": [ … ] }, "clientSaveId": "c7e1…" }
200 OK
{ "draftVersion": 315 }
If-Match carries the version this device last saw, and the editor service turns it into one conditional write: write only if the stored version is still 314. That is the whole concurrency model, at p95 ≤ 300 ms and about 1,560 saves/s at peak.
The interesting case is the second device. Maya's phone went offline on the streetcar holding version 311, and her laptop has since moved the draft to 315. When the phone reconnects:
The 412 Precondition Failed body carries the current version and its metadata, {currentVersion: 315, updatedAt, deviceLabel: "MacBook, 9:42 p.m.", lastSaveId}, so the phone can say edited on your MacBook instead of a bare error. The phone files its unsynced copy with POST …/revisions, fetches the laptop's version with GET …/draft, and Maya picks. Keeping hers is a PUT with If-Match: "315". Nothing is overwritten silently; Section 7's autosave dive covers how lastSaveId decides when the dialog appears.
clientSaveId handles a smaller problem. If the laptop's save of 315 commits but the response is lost, the retry still carries If-Match: "314" and would get a 412 against its own write. The server sees the same clientSaveId on the save that produced 315 and returns 200 {draftVersion: 315} instead.
Quick check before moving on:
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson