• v1.0.0 c4ef1948f6

    Rene Nochebuena released this 2026-05-29 09:48:37 -06:00 | 0 commits to main since this release

    v1.0.0

    code.nochebuena.dev/einherjar/web


    Architecture Decisions Resolved

    Decision Outcome
    One module vs. four separate modules One module — four sub-packages always ship together
    web.New middleware order Recover → RequestID → RequestLogger → CORS (outermost first)
    CORS auto-apply Only when AllowedOrigins is non-empty — off by default
    Rate limit store pattern RateLimiterStore interface (Echo-style) — swap backend without changing middleware
    In-memory cleanup mechanism Background goroutine + time.Ticker (5 min eviction) — no worker dependency
    Rate limit store error policy Fail-open — availability over hard enforcement
    last_seen middleware Excluded — application-domain concern, not transport-level
    Request ID algorithm UUID v7 (time-ordered) with UUID v4 fallback
    health.Checkable definition Imported from contracts/observability — not redefined in health
    httputil error handler Centralized Error(logger, w, r, err) — single log per failed request, level derived from HTTP status
    httputil error log levels ≥500 → Error, 4xx → Warn, 499 → Info — mirrors Echo's centralized handler pattern
    httputil error code coverage 16 codes — complete coverage (adds ErrOutOfRange, ErrAborted, ErrDataLoss, ErrCancelled over micro-lib)
    Lifecycle error types xerrors.Unavailable for bind failure, xerrors.Internal for shutdown failure — logz auto-enriches
    Startup log message "server: listening" emitted after goroutine launch — confirms port is bound and accepting

    API

    Root package

    import "code.nochebuena.dev/einherjar/web"
    
    // Types
    type Config struct {
        Server         server.Config
        AllowedOrigins []string `env:"EINHERJAR_SERVER_CORS_ORIGINS" envSeparator:","`
    }
    
    // Constructor
    func New(logger logging.Logger, cfg ...Config) server.Server
    
    // Environment
    // EINHERJAR_SERVER_CORS_ORIGINS=https://a.com,https://b.com   — comma-separated; empty = CORS off
    // (all server.Config env vars also apply — see server package)
    

    server

    import "code.nochebuena.dev/einherjar/web/server"
    
    // Interface
    type Server interface {
        lifecycle.Component        // OnInit, OnStart, OnStop
        observability.Identifiable // ModulePath, ModuleVersion — read by launcher banner
        chi.Router                 // Mount, Get, Post, Put, Delete, Patch, Use, ...
    }
    
    // Types
    type Config struct {
        Host            string        `env:"EINHERJAR_SERVER_HOST"             envDefault:"0.0.0.0"`
        Port            int           `env:"EINHERJAR_SERVER_PORT"             envDefault:"8080"`
        ReadTimeout     time.Duration `env:"EINHERJAR_SERVER_READ_TIMEOUT"     envDefault:"5s"`
        WriteTimeout    time.Duration `env:"EINHERJAR_SERVER_WRITE_TIMEOUT"    envDefault:"10s"`
        IdleTimeout     time.Duration `env:"EINHERJAR_SERVER_IDLE_TIMEOUT"     envDefault:"120s"`
        ShutdownTimeout time.Duration `env:"EINHERJAR_SERVER_SHUTDOWN_TIMEOUT" envDefault:"10s"`
    }
    type Option func(*opts)
    
    // Constructors
    func New(logger logging.Logger, cfg Config, opts ...Option) Server
    func WithMiddleware(mw ...func(http.Handler) http.Handler) Option
    
    // Lifecycle behaviour
    // OnInit  — applies registered middleware to the router
    // OnStart — binds TCP listener synchronously; logs "server: listening" on success;
    //           returns xerrors.ErrUnavailable on bind failure
    // OnStop  — graceful http.Server.Shutdown within ShutdownTimeout;
    //           returns xerrors.ErrInternal on unexpected shutdown failure
    
    // Environment
    // EINHERJAR_SERVER_HOST=0.0.0.0            — bind address (default 0.0.0.0)
    // EINHERJAR_SERVER_PORT=8080               — listen port (default 8080)
    // EINHERJAR_SERVER_READ_TIMEOUT=5s         — HTTP read timeout (default 5s)
    // EINHERJAR_SERVER_WRITE_TIMEOUT=10s       — HTTP write timeout (default 10s)
    // EINHERJAR_SERVER_IDLE_TIMEOUT=120s       — keep-alive idle timeout (default 120s)
    // EINHERJAR_SERVER_SHUTDOWN_TIMEOUT=10s    — graceful shutdown budget (default 10s)
    

    mw

    import "code.nochebuena.dev/einherjar/web/mw"
    
    // Types
    type StatusRecorder struct {
        http.ResponseWriter
        Status int
    }
    func (r *StatusRecorder) WriteHeader(code int)
    
    type RateLimiterStore interface {
        Allow(ctx context.Context, key string) (bool, error)
    }
    
    type InMemoryRateLimiterStore struct { /* unexported */ }
    func NewInMemoryRateLimiterStore(rps float64, burst int) *InMemoryRateLimiterStore
    func (s *InMemoryRateLimiterStore) Allow(ctx context.Context, key string) (bool, error)
    
    // Middleware constructors
    func Recover() func(http.Handler) http.Handler
    func RequestID(generator func() string) func(http.Handler) http.Handler
    func RequestLogger(logger logging.Logger) func(http.Handler) http.Handler
    func CORS(origins []string) func(http.Handler) http.Handler
    func CORSAllowAll() func(http.Handler) http.Handler
    func IPRateLimit(store RateLimiterStore, logger logging.Logger) func(http.Handler) http.Handler
    func UserRateLimit(store RateLimiterStore, logger logging.Logger) func(http.Handler) http.Handler
    

    httputil

    import "code.nochebuena.dev/einherjar/web/httputil"
    
    // Types
    type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
    func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request)
    // Note: ServeHTTP writes the error response without logging (no logger in scope).
    // Use Handle/HandleNoBody/HandleEmpty for centralized logging, or call Error explicitly.
    
    // Generic handler adapters — centralized logging included
    func Handle[Req, Res any](v valid.Validator, logger logging.Logger, fn func(ctx context.Context, req Req) (Res, error)) http.HandlerFunc
    func HandleNoBody[Res any](logger logging.Logger, fn func(ctx context.Context) (Res, error)) http.HandlerFunc
    func HandleEmpty[Req any](v valid.Validator, logger logging.Logger, fn func(ctx context.Context, req Req) error) http.HandlerFunc
    
    // Centralized error handler
    // Logs at the level derived from HTTP status, then writes the standardized JSON body.
    //   ≥500 → Error  (logz auto-enriches with error_code and WithContext fields)
    //   4xx  → Warn   (client mistake — not a server failure)
    //   499  → Info   (client cancelled intentionally)
    func Error(logger logging.Logger, w http.ResponseWriter, r *http.Request, err error)
    
    // Response helpers
    func JSON(w http.ResponseWriter, status int, v any)
    func NoContent(w http.ResponseWriter)
    

    health

    import "code.nochebuena.dev/einherjar/web/health"
    
    // Types
    type Config struct {
        CheckTimeout time.Duration `env:"EINHERJAR_HEALTH_CHECK_TIMEOUT" envDefault:"5s"`
    }
    type Response struct {
        Status     string                     `json:"status"`
        Components map[string]ComponentStatus `json:"components"`
    }
    type ComponentStatus struct {
        Status  string `json:"status"`
        Latency string `json:"latency,omitempty"`
        Error   string `json:"error,omitempty"`
    }
    
    // Constructors
    func NewHandler(logger logging.Logger, checks ...observability.Checkable) http.Handler
    func NewHandlerWithConfig(logger logging.Logger, cfg Config, checks ...observability.Checkable) http.Handler
    
    // Environment
    // EINHERJAR_HEALTH_CHECK_TIMEOUT=5s   — maximum time to wait for all checks (default 5s)
    

    Install

    go get code.nochebuena.dev/einherjar/web@v1.0.0
    

    Dependencies

    Module Version Role
    code.nochebuena.dev/einherjar/contracts v1.0.0 Interface definitions
    code.nochebuena.dev/einherjar/core v1.0.0 logz, xerrors, valid
    github.com/go-chi/chi/v5 v5.2.1 HTTP router (embedded in Server interface)
    github.com/google/uuid v1.6.0 UUID v7/v4 request ID generation
    golang.org/x/time v0.11.0 Token bucket for InMemoryRateLimiterStore
    Downloads