docs(httpserver): correct tier from 4 to 3
httpserver depends on launcher (Tier 2), placing it at Tier 3. With launcher corrected from Tier 5 to Tier 2, httpserver's tier drops accordingly.
This commit is contained in:
85
CLAUDE.md
Normal file
85
CLAUDE.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user