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:
2026-03-19 13:39:19 +00:00
commit 1ec0780f72
15 changed files with 750 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
# ADR-001: chi over Echo
**Status:** Accepted
**Date:** 2026-03-18
## Context
httpserver needs an HTTP router. The two leading candidates were:
- **chi** (`github.com/go-chi/chi/v5`) — a lightweight, idiomatic router whose root type implements `http.Handler` and whose middleware signature is `func(http.Handler) http.Handler`
- **Echo** — a full web framework with its own `Context` type, its own middleware signature (`echo.MiddlewareFunc`), and handler signature (`echo.HandlerFunc`)
The project constraint is that all transport-layer modules must compose cleanly with the rest of the micro-lib stack, which is built entirely on stdlib `net/http` types. `httpmw` middleware, `httputil` handler adapters, and all application handlers use `http.Handler` and `http.ResponseWriter`/`*http.Request` directly.
## Decision
Use chi. Every chi middleware is `func(http.Handler) http.Handler`, every chi handler is `http.Handler`, and `chi.Router` itself satisfies `http.Handler`. This means:
- Any middleware written for the stdlib works with chi without wrapping.
- Any `http.Handler` can be registered directly without conversion.
- `httputil.Handle` and `httputil.HandleNoBody` adapters slot in without adapter code.
- The server itself can be tested with `httptest.NewRecorder` against the router directly, without starting a process.
Echo would require wrapping every stdlib handler and middleware at the boundary, creating a permanent impedance mismatch between httpserver and the rest of the stack.
## Consequences
- All route handlers and middleware in this project must use stdlib `net/http` types. No Echo-style context is available.
- chi's URL parameter API (`chi.URLParam(r, "id")`) is used for path parameters rather than a framework-specific context method.
- The chi dependency is a direct, visible import in `httpserver.go` and in any application that calls `srv.Route(...)` with a `func(chi.Router)` callback. This is accepted as the deliberate, explicit contract of the embedded router design (see ADR-002).

View File

@@ -0,0 +1,52 @@
# ADR-002: Embedded chi.Router in HttpServerComponent
**Status:** Accepted
**Date:** 2026-03-18
## Context
`HttpServerComponent` must expose routing methods to callers so they can register routes before the server starts. There are two design options:
1. **Wrapper methods** — define `Get(pattern, handler)`, `Post(...)`, `Route(...)`, `Mount(...)`, `Use(...)`, etc. on `HttpServerComponent` explicitly, delegating each to an internal router.
2. **Embedded router** — embed `chi.Router` directly in the interface and the concrete struct, so all chi routing methods are promoted to the component surface automatically.
The application bootstrap pattern in this project registers routes in a `lc.BeforeStart(...)` hook, after `db.OnInit` has run but before `srv.OnStart` binds the port. This means the same value returned by `httpserver.New(...)` is used both as a `launcher.Component` (lifecycle) and as a router (route registration). Callers write:
```go
srv := httpserver.New(logger, cfg, httpserver.WithMiddleware(...))
lc.Append(db, srv)
lc.BeforeStart(func() error {
srv.Get("/health", healthHandler)
srv.Route("/api/v1", func(r chi.Router) { ... })
return nil
})
```
## Decision
Embed `chi.Router` directly in both the `HttpServerComponent` interface and the `httpServer` concrete struct. The interface is declared as:
```go
type HttpServerComponent interface {
launcher.Component
chi.Router
}
```
The struct embeds `chi.Router` as an anonymous field, initialized to `chi.NewRouter()` in `New()`.
This means callers receive the complete chi routing API — `Get`, `Post`, `Put`, `Delete`, `Patch`, `Head`, `Options`, `Route`, `Mount`, `Use`, `With`, `Group`, `Handle`, `HandleFunc`, `Method`, `MethodFunc`, `Connect`, `Trace`, `NotFound`, `MethodNotAllowed` — without any wrapper boilerplate and without any risk of an incomplete wrapper missing a method.
The compliance test (`compliance_test.go`) asserts at compile time:
```go
var _ launcher.Component = httpserver.New(...)
var _ chi.Router = httpserver.New(...)
```
## Consequences
- `chi` is a visible part of the `HttpServerComponent` API. Callers using `Route(...)` or `Mount(...)` must import `github.com/go-chi/chi/v5` for the callback type. This is intentional and explicit, not a leaky abstraction — httpserver is documented as chi-backed.
- A future swap to a different router would be a breaking change to `HttpServerComponent`. This is accepted: the router choice is a deliberate, long-term decision (see ADR-001).
- Writing wrapper methods would have to be updated every time chi adds a new method. Embedding avoids that maintenance burden permanently.
- Middleware is applied to the embedded router in `OnInit()` by calling `s.Router.Use(mw)` for each registered middleware, preserving chi's standard middleware chaining semantics.

View File

@@ -0,0 +1,41 @@
# ADR-003: No Middleware Installed by Default
**Status:** Accepted
**Date:** 2026-03-18
## Context
Many HTTP frameworks and server libraries install a default middleware stack — request logging, panic recovery, CORS headers, request ID injection — on the assumption that most applications will want these. The question is whether `httpserver.New(...)` should do the same.
Arguments for a default stack:
- Reduces boilerplate for the common case.
- Ensures recovery from panics is always in place.
Arguments against:
- Different applications need different middleware in different orders. Order matters: request ID must precede the logger so the logger can attach the ID; auth must precede RBAC; compression must follow auth.
- Installing middleware that the caller neither requested nor knows about makes the system harder to reason about and test.
- The micro-lib design principle is zero-surprise construction: `New()` produces a minimal, predictable value. Behavior is added explicitly.
- A middleware like `RequestLogger` needs a logger argument that `httpserver` would have to accept on behalf of `httpmw`. This couples configuration surface areas that should be independent.
## Decision
`httpserver.New(...)` installs no middleware. The router returned from `New()` is a bare `chi.NewRouter()`. Middleware is composed explicitly by the caller using `WithMiddleware(...)`:
```go
srv := httpserver.New(logger, cfg,
httpserver.WithMiddleware(
httpmw.RequestID(uuid.NewString),
httpmw.Recover(),
httpmw.RequestLogger(&logAdapter{logger}),
),
)
```
`WithMiddleware` is a variadic functional option that appends middleware to an internal slice. In `OnInit()`, each middleware is applied to the router via `s.Router.Use(mw)` in registration order (first registered = outermost in the chain).
## Consequences
- Every application must explicitly compose its middleware stack. This is intentional: the stack is visible, ordered, and testable in the application source.
- Panic recovery is not automatic. If a caller omits `httpmw.Recover()`, unhandled panics will crash the process. This is a deliberate trade-off — it makes the stack explicit and avoids silent behavior.
- Adding middleware after `OnInit()` has run is permitted by chi but is not the intended usage pattern. Route registration and middleware composition should both happen in the `BeforeStart` hook (before `OnStart` binds the port).
- The `WithMiddleware` option appends, so multiple calls accumulate: `WithMiddleware(a, b)` followed by `WithMiddleware(c)` results in `[a, b, c]` applied in that order.