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.
4.4 KiB
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 interfacegithub.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.Handlereverywhere. Every middleware in the micro-lib stack (httpmw) and every handler adapter (httputil) useshttp.Handler/http.ResponseWriter/*http.Request. Echo's custom context type would require wrapper code at every boundary. - Embedded
chi.Router(ADR-002):HttpServerComponentembedschi.Routerdirectly, so callers get the full routing API (Get,Post,Route,Mount,Use,With,Group, etc.) without a wrapper. The concretehttpServerstruct embedschi.NewRouter(). - No default middleware (ADR-003):
New()installs nothing. Middleware is composed explicitly withWithMiddleware(...). Order is caller-controlled and visible in the application source. - Duck-typed Logger (global ADR-001): The
Loggerinterface requires onlyInfo(msg string, args ...any)andError(msg string, err error, args ...any). Any struct with those two methods satisfies it — includinglogz.Loggerand application-local adapters. - Graceful shutdown:
OnStop()callshttp.Server.Shutdownwith a 10-second timeout derived fromcontext.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 afterOnInit()has run — middleware registered afterOnInitmay not behave as expected depending on chi's internal state. Register all middleware viaWithMiddleware(...)at construction time. - Do not register routes outside of a
BeforeStarthook 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. Thechi.Routerembed is part of the public contract. - Do not pass
nilas theLogger. 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 withhttpmwandhttputil.
Testing Notes
httpserver_test.goruns as a white-box test (package httpserver) and useshttptest.NewRequest/httptest.NewRecorderto test routes without binding a port.compliance_test.gois a black-box test (package httpserver_test) containing only compile-time assertions thathttpserver.New(...)satisfies bothlauncher.Componentandchi.Router.- Tests that need a real TCP listener use
freePort(t)to find an available port and callOnStart()/OnStop()directly. - The
Loggerinterface is minimal enough that a two-method struct in the test file satisfies it without any imports from logz.