keylightd shipped its HTTP API on the standard library's http.ServeMux, hand-marshalling JSON in each handler. That's fine until it isn't: the routes drift from the docs, every handler repeats the same decode/encode dance, and "what does this endpoint actually return?" is answered by reading the source. So the API got a proper rebuild.
The handlers now run on Chi + Huma, which buys three things at once:
- Typed requests and responses. Each endpoint is a Go struct in and a Go struct out; the framework handles validation and serialisation. No more manual JSON plumbing, and the compiler catches the shape errors I used to find at runtime.
- An OpenAPI spec for free. Because the types are the contract, the spec is generated from them: it can't drift from the implementation, because it's derived from it. Anything that speaks OpenAPI can now generate a client.
- A middleware ecosystem. Rate limiting, structured logging and the rest drop in as Chi middleware instead of bespoke wrappers.
The rule I held myself to: all thirteen existing routes preserved, byte-for-byte. This is infrastructure plumbing (a tray app and a GNOME extension are on the other end) so a rebuild that quietly changed a response shape would be a bug, not an upgrade. Same contract, better foundations.
The same pass added WebSocket events: instead of polling the daemon to ask "is the light on yet?", a client can subscribe and get state changes pushed to it. The GNOME extension reflecting a light you toggled from the CLI, instantly, is the kind of small thing that only happens once the daemon can tell you rather than wait to be asked.
Logging moved to structured slog with hot-reloadable filters in the same sweep: the boring, load-bearing stuff. Repo and docs on GitHub.
