4.6 KiB
fullstack-template backend
Go 1.27 API server template: stdlib net/http routing (Go 1.22+ method
patterns with r.PathValue), Bun ORM (SQLite
file by default, Postgres via DATABASE_URL), and a layered layout
(handler → service → repository behind interfaces) designed to be copied
per resource and iterated on with an LLM.
Quickstart
No containers, no database server — the default database is a local SQLite
file (dev.db):
cd backend
make migrate-up # apply migrations (creates dev.db)
make run # API on :8080
Try it:
curl -s localhost:8080/api/v1/items
curl -s -X POST localhost:8080/api/v1/items \
-H 'Content-Type: application/json' \
-d '{"title":"First item","description":"Hello template"}'
curl -s localhost:8080/api/v1/items/1
curl -s -X PATCH localhost:8080/api/v1/items/1 \
-H 'Content-Type: application/json' -d '{"status":"archived"}'
curl -s -i -X DELETE localhost:8080/api/v1/items/1
Endpoints
| Method | Path | Notes |
|---|---|---|
| GET | /healthz |
liveness |
| GET | /readyz |
checks the database |
| GET | /api/auth/session |
who is signed in; always 200, {"authenticated":bool,"user":…|null} |
| GET | /api/auth/login |
starts the OIDC redirect; optional ?returnTo=/path |
| GET | /api/auth/callback |
issuer lands here; do not call directly |
| POST | /api/auth/logout |
clears the session cookie; 204 |
| GET | /api/v1/items |
`?status=active |
| POST | /api/v1/items |
201 + Location, body {"title","description","status"} |
| GET | /api/v1/items/{id} |
|
| PATCH | /api/v1/items/{id} |
partial update; null-absent fields untouched |
| DELETE | /api/v1/items/{id} |
204 |
Errors are always {"error":{"code":"...","message":"..."}}.
OIDC authentication
Set OIDC_CLIENT_ID / OIDC_CLIENT_SECRET (see .env.example)
and the API authenticates against the issuer at OIDC_ISSUER
(default https://users.k6n.net) using the authorization-code + PKCE flow:
- The browser is sent to the issuer from
GET /api/auth/login; the callback path is fixed at/api/auth/callback. The redirect URI is derived from the incoming request (X-Forwarded-Host/X-Forwarded-Protowhen behind the Vite proxy,Host/TLS otherwise), so the same server works onlocalhost:<any port>(loopback exception),*.preview.k6n.net(registered wildcard), and the API origin itself — as long as those URIs are registered for the client with the issuer. - The signed-in identity (sub, name, email, roles) is kept in an
HMAC-SHA256-signed session cookie (
HttpOnly,SameSite=Lax). ID and access tokens never reach the browser and are not persisted server-side; - When auth is enabled, everything under
/api/v1/answers401 unauthorizedwithout a valid session./healthz,/readyz, and/api/auth/*stay public. - Without
OIDC_CLIENT_IDthe server runs standalone: no/api/authroutes and an open/api/v1(logged loudly at boot).
AUTH_SESSION_SECRET signs the cookies — unset it only in development (a
random one is generated per boot, invalidating all sessions on restart).
AUTH_SESSION_TTL (default 168h) bounds the session.
Layout
cmd/
api/ server entrypoint (config, wiring, graceful shutdown)
migrate/ migration CLI: up | down | status | new
internal/
api/ routing, middleware, JSON conventions, item + auth handlers
auth/ OIDC client (code + PKCE flow) and signed session cookies
config/ env parsing — the only place reading os.Getenv
database/ bun connection (sqlite or postgres) + migrator
migrations/ one Go file per migration: <timestamp>_<name>.go
items/ reference resource: model, Repository interface,
Bun implementation, service (business rules)
compose.yaml optional dev Postgres
Makefile `make check` = fmt-check + vet + test
internal/items is the pattern to copy when adding a resource — see
AGENTS.md for the step-by-step recipe.
Configuration
All settings come from the environment and a .env file in backend/ is
autoloaded. The database defaults to the SQLite file dev.db; for
Postgres run make db-up and set
DATABASE_URL=postgres://fullstack:fullstack@localhost:5432/fullstack?sslmode=disable.
See .env.example for the full list.
Tests
make test # or: go test ./...
Service and handler tests run against in-memory fakes; the database test runs the real migrations and repository against in-memory SQLite — no database server needed for any of them.