April 12, 2026
API design choices for a desktop app
The Tokori HTTP API runs on localhost, behind a bearer token that lives at ~/.tokori/api-token. It looks REST-ish. Most of the conventions came from "what would a developer expect?" — a few came from "what would actively annoy them?" A short tour.
What we kept from REST
- Resource-oriented URLs.
/v1/workspaces/:id/vocab, not/v1/getVocabFor?ws=.... People read URLs out loud. - HTTP verbs do what they say.
GETis safe.POSTcreates.PATCHis partial.DELETEdeletes. We don't redefine them. - Versioned root. Everything lives under
/v1. When we break compatibility, it's a new prefix, not a silent shape change. - Standard status codes. 401 means "wrong key", 403 means "right key, wrong permission", 404 means "not here", 422 means "validation". One pattern across the whole surface.
What we ignored
- HATEOAS. Nobody writes clients that traverse hypermedia. The docs are the entry point.
- PUT for full-replace. We rarely needed it. PATCH does almost every update we have.
- Nested representations. By default, list endpoints don't embed children. You opt in with
?include=variantswhen you actually want them. That keeps the common case cheap.
Choices specific to "this lives on localhost"
- No OAuth. A single bearer token, generated locally, rotatable with one CLI command. There's no remote service to log into.
- CORS allows loopback by default. Browser extensions and local web tools should "just work". The token gates anything malicious.
- Read-only mode is a flag, not a separate token.
?readonly=1on any request. Useful for piping data into tools you don't fully trust. - SSE for streaming, not WebSockets. Same direction, fewer moving parts, works through every reverse proxy. Chat deltas come out as
text/event-stream.
Errors are an envelope, not a status code
Every error returns the same shape: { error: { code, message, field?, request_id } }. The status code communicates the kind, the code string is what your client matches on. Codes are namespaced (vocab.duplicate_word, chat.upstream_error) and stable across patch releases. That makes switch blocks in client code possible without grepping bodies.
Pagination is cursor-based, always
Lists return { data, next_cursor? }. Cursors are opaque base64 strings — clients should not try to parse them. We chose cursors over offsets because vocab lists grow and resort, and offset-pagination would visibly skip rows when something between pages got deleted mid-walk.
What we still don't know
We don't yet have a great answer for batch writes. Adding 5,000 vocab entries means 5,000 round-trips today. That's fine on localhost (microseconds each), but inelegant. A future POST /v1/workspaces/:id/vocab/batch might happen if anyone asks.
The full reference lives in the main Tokori repo on GitHub. Open an issue there if anything looks wrong, surprising, or missing.