Files
httpserver/CLAUDE.md

86 lines
4.4 KiB
Markdown
Raw Normal View History

# httpserver
Lifecycle-managed HTTP server built on chi, implementing `launcher.Component` and embedding `chi.Router`.
## Purpose
Provides an HTTP server that integrates with the `launcher` lifecycle (OnInit → OnStart → OnStop) while exposing the full chi routing API directly. Callers use a single value for both lifecycle management and route registration.
## Tier & Dependencies
**Tier 3** (transport layer). Depends on:
- `code.nochebuena.dev/go/launcher` — Component interface
- `github.com/go-chi/chi/v5` — router (part of the public API)
No application-level or business-logic dependencies. Do not import domain, service, or repository packages from this module.
## Key Design Decisions
- **chi over Echo** (ADR-001): chi uses stdlib `http.Handler` everywhere. Every middleware in the micro-lib stack (`httpmw`) and every handler adapter (`httputil`) uses `http.Handler`/`http.ResponseWriter`/`*http.Request`. Echo's custom context type would require wrapper code at every boundary.
- **Embedded `chi.Router`** (ADR-002): `HttpServerComponent` embeds `chi.Router` directly, so callers get the full routing API (`Get`, `Post`, `Route`, `Mount`, `Use`, `With`, `Group`, etc.) without a wrapper. The concrete `httpServer` struct embeds `chi.NewRouter()`.
- **No default middleware** (ADR-003): `New()` installs nothing. Middleware is composed explicitly with `WithMiddleware(...)`. Order is caller-controlled and visible in the application source.
- **Duck-typed Logger** (global ADR-001): The `Logger` interface requires only `Info(msg string, args ...any)` and `Error(msg string, err error, args ...any)`. Any struct with those two methods satisfies it — including `logz.Logger` and application-local adapters.
- **Graceful shutdown**: `OnStop()` calls `http.Server.Shutdown` with a 10-second timeout derived from `context.WithTimeout(context.Background(), 10*time.Second)`. In-flight requests have up to 10 seconds to complete before the server returns.
## Patterns
**Construction and wiring:**
```go
srv := httpserver.New(logger, httpserver.Config{Port: 3000},
httpserver.WithMiddleware(
httpmw.RequestID(uuid.NewString),
httpmw.Recover(),
httpmw.RequestLogger(logger),
),
)
lc.Append(db, srv)
```
**Route registration in BeforeStart (after db init, before port bind):**
```go
lc.BeforeStart(func() error {
srv.Get("/health", healthHandler)
srv.Route("/api/v1", func(r chi.Router) {
r.Get("/todos", todoHandler.FindAll)
r.Post("/todos", todoHandler.Create)
})
return nil
})
```
**Middleware applied to a sub-group:**
```go
srv.Group(func(r chi.Router) {
r.Use(appMW.Auth(userRepo))
r.With(appMW.Require(permProvider, resource, perm)).Get("/protected", handler)
})
```
**Config env vars:**
| Variable | Default | Description |
|---|---|---|
| `SERVER_HOST` | `0.0.0.0` | Bind address |
| `SERVER_PORT` | `8080` | Listen port |
| `SERVER_READ_TIMEOUT` | `5s` | Read timeout |
| `SERVER_WRITE_TIMEOUT` | `10s` | Write timeout |
| `SERVER_IDLE_TIMEOUT` | `120s` | Keep-alive idle timeout |
## What to Avoid
- Do not call `srv.Use(mw)` directly after `OnInit()` has run — middleware registered after `OnInit` may not behave as expected depending on chi's internal state. Register all middleware via `WithMiddleware(...)` at construction time.
- Do not register routes outside of a `BeforeStart` hook when using the launcher. Routes should be registered after dependent components (e.g., the database) have initialized.
- Do not add a `chi`-incompatible router behind this interface. The `chi.Router` embed is part of the public contract.
- Do not pass `nil` as the `Logger`. The logger is called on every start, stop, and fatal error.
- Do not use Echo, Gin, or any framework that wraps `http.Handler` — it will be incompatible with `httpmw` and `httputil`.
## Testing Notes
- `httpserver_test.go` runs as a white-box test (`package httpserver`) and uses `httptest.NewRequest` / `httptest.NewRecorder` to test routes without binding a port.
- `compliance_test.go` is a black-box test (`package httpserver_test`) containing only compile-time assertions that `httpserver.New(...)` satisfies both `launcher.Component` and `chi.Router`.
- Tests that need a real TCP listener use `freePort(t)` to find an available port and call `OnStart()` / `OnStop()` directly.
- The `Logger` interface is minimal enough that a two-method struct in the test file satisfies it without any imports from logz.