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:
30
docs/adr/ADR-001-chi-over-echo.md
Normal file
30
docs/adr/ADR-001-chi-over-echo.md
Normal 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).
|
||||
52
docs/adr/ADR-002-embedded-chi-router.md
Normal file
52
docs/adr/ADR-002-embedded-chi-router.md
Normal 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.
|
||||
41
docs/adr/ADR-003-no-default-middleware.md
Normal file
41
docs/adr/ADR-003-no-default-middleware.md
Normal 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.
|
||||
Reference in New Issue
Block a user