115 lines
4.6 KiB
Markdown
115 lines
4.6 KiB
Markdown
# fullstack-template backend
|
|
|
|
Go 1.27 API server template: stdlib `net/http` routing (Go 1.22+ method
|
|
patterns with `r.PathValue`), [Bun](https://bun.uptrace.dev) 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`):
|
|
|
|
```sh
|
|
cd backend
|
|
make migrate-up # apply migrations (creates dev.db)
|
|
make run # API on :8080
|
|
```
|
|
|
|
Try it:
|
|
|
|
```sh
|
|
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|archived&limit=&offset=` — requires a session when auth is on |
|
|
| 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](.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-Proto` when behind the
|
|
Vite proxy, `Host`/TLS otherwise), so the same server works on
|
|
`localhost:<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/` answers
|
|
`401 unauthorized` without a valid session. `/healthz`, `/readyz`, and
|
|
`/api/auth/*` stay public.
|
|
- Without `OIDC_CLIENT_ID` the server runs standalone: no `/api/auth`
|
|
routes 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](.env.example) for the full list.
|
|
|
|
## Tests
|
|
|
|
```sh
|
|
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.
|