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

116
httpserver.go Normal file
View File

@@ -0,0 +1,116 @@
package httpserver
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"code.nochebuena.dev/go/launcher"
)
// Logger is the minimal interface httpserver needs — satisfied by logz.Logger.
type Logger interface {
Info(msg string, args ...any)
Error(msg string, err error, args ...any)
}
// Config holds the HTTP server configuration.
type Config struct {
Host string `env:"SERVER_HOST" envDefault:"0.0.0.0"`
Port int `env:"SERVER_PORT" envDefault:"8080"`
ReadTimeout time.Duration `env:"SERVER_READ_TIMEOUT" envDefault:"5s"`
WriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT" envDefault:"10s"`
IdleTimeout time.Duration `env:"SERVER_IDLE_TIMEOUT" envDefault:"120s"`
}
// serverOpts holds functional options.
type serverOpts struct {
middleware []func(http.Handler) http.Handler
}
// Option configures an httpserver component.
type Option func(*serverOpts)
// WithMiddleware applies one or more middleware to the root chi router during OnInit.
func WithMiddleware(mw ...func(http.Handler) http.Handler) Option {
return func(o *serverOpts) {
o.middleware = append(o.middleware, mw...)
}
}
// HttpServerComponent is a launcher.Component that also exposes a chi.Router.
// Embedding chi.Router gives callers the full routing API: Get, Post, Route, Mount, Use, etc.
type HttpServerComponent interface {
launcher.Component
chi.Router
}
type httpServer struct {
chi.Router
logger Logger
cfg Config
opts serverOpts
srv *http.Server
}
// New creates an HttpServerComponent. No middleware is installed by default;
// use [WithMiddleware] to compose the middleware stack.
func New(logger Logger, cfg Config, opts ...Option) HttpServerComponent {
o := serverOpts{}
for _, opt := range opts {
opt(&o)
}
return &httpServer{
Router: chi.NewRouter(),
logger: logger,
cfg: cfg,
opts: o,
}
}
// OnInit applies registered middleware to the router. No-op if none provided.
func (s *httpServer) OnInit() error {
for _, mw := range s.opts.middleware {
s.Router.Use(mw)
}
return nil
}
// OnStart starts the HTTP server in a background goroutine.
func (s *httpServer) OnStart() error {
host := s.cfg.Host
if host == "" {
host = "0.0.0.0"
}
port := s.cfg.Port
if port == 0 {
port = 8080
}
s.srv = &http.Server{
Addr: fmt.Sprintf("%s:%d", host, port),
Handler: s.Router,
ReadTimeout: s.cfg.ReadTimeout,
WriteTimeout: s.cfg.WriteTimeout,
IdleTimeout: s.cfg.IdleTimeout,
}
s.logger.Info("httpserver: starting", "addr", s.srv.Addr)
go func() {
if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("httpserver: fatal error", err)
}
}()
return nil
}
// OnStop performs a graceful shutdown, waiting up to 10 seconds for in-flight
// requests to complete.
func (s *httpServer) OnStop() error {
s.logger.Info("httpserver: shutting down gracefully")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return s.srv.Shutdown(ctx)
}