Files
httpserver/CLAUDE.md
Rene Nochebuena 1ec0780f72 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.
2026-03-19 13:39:19 +00:00

4.4 KiB

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:

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):

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:

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.