Tutorial example
The finished application from docs/tutorial.md: one
Task resource served through Huma, with cookie auth and the runtime admin.
go run ./examples/tutorial
A superuser is seeded at startup: admin@example.com /
correct-horse-battery-staple.
Why this lives here
The tutorial builds this app with gombit new + gombit make resource, which
produces a separate Go module — generated apps are never committed to this
repository. This copy is written against the framework module directly, so CI
compiles it on every push. If a framework change breaks what the tutorial
teaches, the build fails here first.
It uses the model-first layout make resource now emits: the model is the
human-owned source of truth, the request/response DTOs and CRUD handler are
generated *.gen.go, and customization lives in hooks.go (see
cli.md and the
migration guide). The committed
*.gen.go are kept honest by a drift test (TestGeneratedFilesAreFresh) that
re-derives them from the model with the generator's derivation core
(resourcegen.RenderResource) and fails CI if the model changed without
regenerating. It is an in-process freshness check, not gombit generate --check (this example is part of the framework module, not a standalone app, so
the CLI's Program-Mode discovery/compilation can't target it).
examples/tutorial/
├── main.go # ≈ cmd/server/main.go in a generated app
└── internal/task/
├── task.go # GORM model (the human-owned source of truth)
├── dto.gen.go # generated request/response DTOs + mappers (DO NOT EDIT)
├── handler.gen.go # generated Huma list/get/create + Register (DO NOT EDIT)
├── hooks.go # human-owned BeforeCreate hook (seeded once)
└── admin.go # admin.Register (ADR-013)
Differences from a generated app, all for self-containment:
- an in-memory SQLite DSN instead of a file, and
AutoMigrate in an OnStart
hook instead of gombit db migrate;
- the superuser is seeded in process rather than by
gombit createsuperuser;
- config is built with
config.Default() in code instead of read from .env;
- no
frontend/ — the tutorial's React pages belong to the generated tree.
Trying the API
Cookie auth means writes need a CSRF token:
curl -s -c jar.txt http://127.0.0.1:8083/api/v1/auth/csrf
CSRF=$(grep -i csrf jar.txt | awk '{print $7}')
curl -s -b jar.txt -c jar.txt -X POST http://127.0.0.1:8083/api/v1/auth/login \
-H 'Content-Type: application/json' -H "X-CSRF-Token: $CSRF" \
-d '{"email":"admin@example.com","password":"correct-horse-battery-staple"}'
CSRF=$(grep -i csrf jar.txt | awk '{print $7}')
curl -s -b jar.txt -X POST http://127.0.0.1:8083/api/v1/tasks \
-H 'Content-Type: application/json' -H "X-CSRF-Token: $CSRF" \
-d '{"title":"Write the tutorial","done":false}'
{"data":{"id":1,"created_at":"2026-01-02T15:04:05Z","updated_at":"2026-01-02T15:04:05Z","title":"Write the tutorial","done":false}}
(The model-first response DTO exposes the model's created_at/updated_at,
derived from the model — the hand-written handler this example used to carry
omitted them.)
Then read it back through the admin data plane:
curl -s -b jar.txt 'http://127.0.0.1:8083/api/v1/admin/resources/tasks?per_page=5'
See docs/auth-cookie.md and
docs/admin.md.