A compact, production-conscious REST API example using Huma v2 on Chi. It demonstrates API contracts, JSON and CBOR negotiation, RFC 9457 errors, structured observability, Firebase Authentication, Firestore persistence, and a separate Go Cloud Run function without turning a playground into a framework.
Gopher illustration from free-gophers-pack by Maria Letta.
- Huma v2 typed operations and runtime-generated OpenAPI 3.1
- Stoplight Elements interactive API documentation
- JSON and CBOR requests, responses, and Problem Details
- One Huma error pipeline for operation and Chi-level 404, 405, and recovery responses
- Request IDs, trace metadata, operation-aware access logs, and request-scoped Zap loggers
- Cursor pagination with RFC 8288
Linkheaders - Firebase ID-token verification with revocation checks
- Firestore atomic create, transaction-safe partial update, existence-checked delete, and audit events
- Explicit development-offline, emulator, and live Firebase modes
- Bounded request, upstream response, server, and shutdown work
- Anonymous, public-only GitHub proxy requests without credential forwarding
- Non-root distroless container execution
- A deliberately small, separately deployable Go Functions Framework example
- Required CI execution for both Go modules plus separate emulator-backed coverage
- Go 1.26.5+ (the repository currently pins 1.26.5)
- Just
- golangci-lint v2
- actionlint (
brew install actionlint) - Firebase CLI and Java 21 when running emulator integration tests
- Docker or Podman for container checks
Copy the example environment:
cp .env.example .envThe default configuration is safe for local exploration: the public API starts without cloud credentials and protected Firebase routes return 503 Service Unavailable.
just install
just runOpen:
http://localhost:8080/healthhttp://localhost:8080/v1/api-docshttp://localhost:8080/v1/openapi.jsonhttp://localhost:8080/v1/openapi.yaml
Example:
curl --fail --silent http://localhost:8080/v1/hello
curl --fail --silent \
-H 'Content-Type: application/json' \
-d '{"name":"Ada"}' \
http://localhost:8080/v1/hello| Variable | Default | Purpose |
|---|---|---|
HOST |
0.0.0.0 |
Listen host |
PORT |
8080 |
Listen port |
APP_ENVIRONMENT |
development |
development, staging, or production |
LOG_LEVEL |
info |
debug, info, warn, or error |
FIREBASE_MODE |
offline |
offline, emulator, or live |
FIREBASE_PROJECT_ID |
demo-test-project outside live mode |
Firebase project |
FIREBASE_AUTH_EMULATOR_HOST |
unset | Auth emulator address |
FIRESTORE_EMULATOR_HOST |
unset | Firestore emulator address |
CORS_ALLOWED_ORIGINS |
* in development |
Comma-separated browser origins; required outside development |
GOTOOLCHAIN |
set by .env |
Repository Go toolchain pin |
offline is development-only. Public routes work; protected routes fail closed with 503. No Firebase SDK client is created.
emulator is development-only and requires both emulator host variables as valid host:port authorities without a URL scheme or whitespace. Use a demo-* project ID. Partial or malformed emulator configuration is rejected at startup.
live requires a non-demo project and Application Default Credentials. Emulator variables and demo-* projects are rejected. Production and staging also require explicit non-wildcard CORS origins.
These checks prevent accidental use of the Auth emulator outside development, where unsigned test tokens would be unsafe.
firestore.rules denies direct client reads and writes. The Admin SDK bypasses those rules, so the API enforces ownership by deriving a collision-safe profile document path from the verified Firebase UID rather than accepting a user ID from the request.
| Method | Path | Description |
|---|---|---|
| GET | /health |
Liveness probe |
| GET | /v1/hello |
Default greeting |
| POST | /v1/hello |
Generate a personalized greeting |
| GET | /v1/items |
Cursor-paginated static items |
| POST | /v1/profile |
Create the authenticated user's profile |
| GET | /v1/profile |
Read the authenticated user's profile |
| PATCH | /v1/profile |
Partially update the authenticated user's profile |
| DELETE | /v1/profile |
Delete the authenticated user's profile |
| GET | /v1/github/owners/{owner} |
GitHub owner information |
| GET | /v1/github/owners/{owner}/repos |
Up to 30 owner repositories |
| GET | /v1/github/repos/{owner}/{repo} |
Repository details |
| GET | /v1/github/repos/{owner}/{repo}/activity |
Cursor-paginated activity |
| GET | /v1/github/repos/{owner}/{repo}/languages |
Repository language bytes |
| GET | /v1/github/repos/{owner}/{repo}/tags |
Up to 30 tags |
Profile JSON uses camelCase (firstName, lastName, contactEmail, phoneNumber). Firestore uses snake_case (first_name, last_name, contact_email, phone_number). contactEmail is user-supplied and is not the verified Firebase identity email.
Profile creation uses Firestore create-if-absent semantics, partial updates preserve unrelated stored fields, and deletion uses an existence precondition rather than a read-before-delete transaction.
The sample item price is priceMinor plus currency. The integer is expressed in the ISO 4217 currency's minor unit.
Use Accept: application/json or Accept: application/cbor. JSON is the default.
Errors are RFC 9457 Problem Details:
application/problem+jsonapplication/problem+cbor
Huma provides the same schema transformation and content negotiation for operation failures and Chi-level errors. Advertised schema links resolve under /v1/schemas/.
Operation metadata lists only errors reachable for that operation. Unexpected Firebase and GitHub dependency failures are logged once with request correlation and a safe operation name; clients receive generic Problem Details without upstream internals.
Request bodies are limited to 1 MiB. Unknown query parameters and unknown body properties are rejected. Application request contexts expire before the server write timeout so Firebase and GitHub work is canceled within the response budget.
All repository workflows go through Just so .env and GOTOOLCHAIN are applied consistently.
| Command | Purpose |
|---|---|
just build |
Build both Go modules |
just test |
Test both Go modules |
just test-race |
Run both modules with the race detector |
just fuzz |
Fuzz the cursor and GitHub Link-header parsers |
just lint |
Lint both modules |
just fmt |
Format both modules |
just fmt-check |
Reject formatting drift |
just tidy-check |
Reject module-file drift |
just vuln |
Run govulncheck against both modules |
just workflow-check |
Validate GitHub Actions locally with the installed actionlint |
just coverage |
Generate root application coverage reports |
just functions-run |
Run the local Functions Framework target |
just functions-smoke |
Build and probe the registered function target |
just emulators |
Start Auth and Firestore emulators |
just test-integration-ci |
Require emulator-backed tests and generate their separate coverage report |
just container-smoke |
Build and probe the final non-root image |
go.work is optional, local-only convenience. It is ignored intentionally. Every root recipe sets GOWORK=off for the nested function module, so clean clones and CI do not depend on a workspace file.
Start emulators:
just emulatorsPorts:
- Auth:
127.0.0.1:7110 - Firestore:
127.0.0.1:7130 - Emulator UI:
127.0.0.1:4000
Ordinary local tests skip emulator cases when emulators are absent. The required CI recipe sets REQUIRE_FIREBASE_EMULATORS=1, so unavailable or broken emulators fail rather than silently reducing coverage. It writes a separate integration-coverage.* report; CI does not merge that profile with the fast unit report.
functions/ is an independent Go module. The HTTP registration is at the module root beside functions/go.mod, as required by Cloud Run source functions. functions/cmd/server is a local Functions Framework runner.
The example intentionally contains one small handler, one test file, and one runner. It does not import Huma, Firebase Admin, or the application observability stack. Its timestamp layout is deliberately duplicated: sharing one constant would couple two independently built and deployed modules to remove a trivial line.
Run it:
just functions-run
curl --fail --silent 'http://localhost:8080/?name=Ada'The local recipe sets LOCAL_ONLY=true, so the runner binds to 127.0.0.1. Direct runner invocations can omit that variable to use the Functions Framework's all-interface default.
Deploy it as a Cloud Run function, not through Firebase CLI:
gcloud run deploy huma-playground-hello \
--source functions \
--function Hello \
--base-image go126 \
--region REGION \
--allow-unauthenticatedChoose authenticated invocation instead when the function should not be public.
just container-build
just container-smokeThe final image is distroless, statically linked, and runs as UID/GID 65532:65532. The build injects the supplied version into startup logs and OCI labels.
Deploy the already-built image:
gcloud run deploy huma-playground \
--image REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/huma-playground:TAG \
--region REGIONDo not combine this image deployment with --base-image or --automatic-updates. Go standard-library, dependency, and base-image fixes require rebuilding and redeploying the compiled artifact.
.agents/skills/ six portable project workflows with Codex UI metadata
.github/agents/ evidence-based security review profile for GitHub Copilot
cmd/server/ typed config, composition, lifecycle
internal/http/health/ unversioned liveness transport
internal/http/v1/ Huma operations grouped by resource
internal/http/v1/routes/ route composition
internal/platform/auth/ Firebase verification and Huma auth middleware
internal/platform/firebase/ Firebase Admin client initialization
internal/platform/middleware/ HTTP security, CORS, Vary, Chi access logs
internal/platform/pagination/ transport-independent cursor mechanics
internal/platform/respond/ Chi recovery/errors delegated to Huma
internal/platform/timeutil/ fixed-precision JSON/CBOR timestamps
internal/service/github/ bounded GitHub API adapter
internal/service/profile/ Firestore profile store
internal/testutil/ emulator-only test helpers
functions/ independent Functions Framework module
The application uses constructor-style composition and narrow interfaces. It deliberately does not add a DI container, repository framework, generic service layer, in-process distributed rate limiter, cache, or OpenTelemetry dependency.
Repository guidance follows the canonical AGENTS.md format. Portable skills use
the canonical Agent Skills specification and documentation, with the
detailed format specification, under .agents/skills/. See
AGENTS.md for the working rules and current skill catalog.
The app uses github.com/janisto/huma-observability/v2. obs.HTTPRequestContext is installed at the Chi boundary so liveness, recovery, 404, 405, and Huma routes share request IDs, request-scoped loggers, and W3C trace metadata. The HTTP and Huma middleware explicitly use Trace Context Level 1. Huma routes additionally use obs.RequestContext and obs.AccessLogger for operation-aware logs.
Access logs are privacy-minimized: Huma records route templates and operation IDs without raw paths, peer IPs, or user agents; the local Chi logger follows the same boundary and wraps only Chi-only routes and error handlers. Escaping non-abort panics before a response write are recorded as status 500 with terminal_reason: "panic" and error severity. This split also prevents duplicate /v1 access logs. obs.Logger(ctx) is intentionally request-bound; process and background work must receive an explicit logger.
Forwarding headers are removed at the outer HTTP boundary because this example does not define a trusted-proxy boundary; this also prevents forwarded-host values from influencing Huma schema links.
- Bearer tokens and user-supplied profile/greeting values are not logged.
- Prefer local ADC (
gcloud auth application-default login) for live-mode experiments; do not place service-account keys in the repository. - CORS credentials are disabled.
- HSTS belongs at the trusted TLS edge, not this HTTP application.
- Public rate limiting belongs at Cloud Run, API Gateway, or Cloud Armor unless the application gets an identity-aware quota requirement.
/healthis liveness only. Add dependency readiness only for a deployment with a concrete readiness contract.