apistockdocs
Decisions

API contract and documentation

ADR-0027Accepted

Status: Accepted (2026-09-14), amended by ADR-0049, ADR-0051 (Postman collection and llms.txt exported with the spec) · decided by spikes/openapi

Context#

Developers must test the API immediately after aps new: interactive docs at /docs, an OpenAPI document and a Postman collection. Documentation should look like Mintlify API reference pages. All of these need one source of truth, and the choice shapes how every handler is written. The priority is a seamless developer experience: developers focus on business logic, apistock handles contract, docs and validation.

Options#

OptionHow it worksForAgainst
A. Spec-firstOwned openapi.yaml → generated Go interfaces (oapi-codegen) → handlersContract-first; no framework in handlersDevelopers write YAML; two steps per endpoint
B. Code-first (Huma)Typed Go inputs/outputs and handlers generate OpenAPI 3.1Go only; docs and validation automatic; RFC 9457 errors; runs on net/httpThird-party dependency in the HTTP layer
C. Annotations (swaggo)Comments generate specsEasy startComments drift from behaviour

Decision#

Option B: code-first with Huma v2, confined to the delivery/ layer.

TopicDecision
Where Huma is allowedinternal/modules/*/delivery, internal/modules/*/module.go, internal/app. Never domain/, usecase/, repository/ (enforced by architecture_test.go)
RouterStandard http.ServeMux via the humago adapter
HandlersTyped input/output structs; handlers return domain errors unchanged
Errorsinternal/app installs huma.NewError and huma.NewErrorWithContext once at startup, producing Problem (RFC 9457 + code + request_id + errors[]) from the app's error mapping table (ADR-0018). This is the only permitted package-level assignment; the architecture test enforces it
ValidationFrom Go struct tags; 422 validation_failed with field locations
Auth and tenancyPer-operation Huma middlewares for session, org membership and permission; security documented
ConfigCreateHooks = nil (no $schema links in responses); built-in docs disabled
Interactive docsOwn /docs handler, offline with no CDN, Mintlify-style layout with examples and try-it; configurable enabled / disabled / ops-only. Originally an embedded, pinned Scalar asset; since ADR-0049, the apistock reference rendered by modules/openapi/reference
Spec exportmy-api openapi writes api/openapi.json; aps dev refreshes it; committed so API changes appear in pull requests
Breaking changesCI compares api/openapi.json with the base branch and fails on breaking changes
Postman and AIapi/postman_collection.json and api/llms.txt generated from the exported spec (v0.5)
Undocumented routesArchitecture test fails if a route is registered outside Huma operations (except /docs, /livez, /readyz, /.well-known/*)
Time valuesGenerated clocks return UTC
Generatoraps gen resource and aps gen endpoint create input/output types, operation registration, handler and use-case stubs
Project docsapistock.dev/docs built with Mintlify; MDX and OpenAPI kept in this repository

Unknown request fields: tolerant#

Huma rejects unknown request-body fields by default. apistock apps ignore unknown fields (tolerant reader) so older server versions keep accepting requests from newer mobile and web clients. Validation still applies to every known field. Huma has no public global switch (its registry setting is unexported), so the generator adds _ struct{} `json:"-" additionalProperties:"true"` to every request body type, and a template test checks it. Verified in the first-run spike.

Docs asset size#

Embedding Scalar added about 3.5 MB per binary, so the template embedded a pre-compressed asset served with Content-Encoding. Since ADR-0049 the reference is rendered by apistock itself: about 100 KB of fonts plus its own stylesheet and script, under a Content-Security-Policy without 'unsafe-eval' or inline styles. /docs stays configurable (enabled, disabled, ops-only).

Open for v0.4#

  • Non-member responses: 403 vs 404 for organisations the caller doesn't belong to.

Why#

  • Developers write only Go; docs, validation and error schemas can't drift from code.
  • The layered architecture limits the dependency to one layer: replacing Huma would rewrite delivery/ only.
  • Huma is actively maintained (v2.39.1), MIT-licensed, uses the standard router and RFC 9457 errors.
  • The spike showed a single direct dependency and a 6.4 MB stripped binary.

Trade-offs#

  • A third-party library shapes handler signatures in every generated app.
  • Error customisation relies on Huma's package-level hooks.
  • If Huma is abandoned, apistock must fork it or migrate delivery/ layers with a codemod.

Consequences#

  • apistock pins and tests Huma versions; upgrades go through the compatibility matrix.
  • delivery/ code is part of the scaffold compatibility promise (ADR-0016).
  • ADR-0022's delivery layer description refers to Huma operations.
esc
↑↓ move↵ openesc close