Initial commit
This commit is contained in:
commit
afed023152
8152 files changed
+2086461
No files matched your search
@@ -0,0 +1,114 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user