web

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 29, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

README

einherjar/web

version license go

The gate is not a barrier. It is the point where the outside world meets order.

code.nochebuena.dev/einherjar/web is the HTTP layer of the Einherjar framework. It sits above core in the dependency graph and provides everything a service needs to receive, process, and respond to HTTP requests: a lifecycle-aware server, a composable middleware stack, type-safe generic handlers, and a concurrent health endpoint.


Sub-packages

Package Import path Purpose
server .../web/server Lifecycle-aware HTTP server (chi router + lifecycle.Component)
mw .../web/mw Middleware: Recover, RequestID, RequestLogger, CORS, rate limiting
httputil .../web/httputil Generic handler adapters: decode → validate → call → encode
health .../web/health Concurrent health-check endpoint consuming observability.Checkable

All four are in one module because they compose together and ship in every Einherjar HTTP service.


Usage

Tier 1 — Happy path (web.New)

Zero config, safe defaults, all env vars respected:

import (
    "code.nochebuena.dev/einherjar/core/logz"
    "code.nochebuena.dev/einherjar/web"
    "code.nochebuena.dev/einherjar/web/health"
)

logger := logz.New(logz.Config{JSON: true, StaticArgs: []any{"service", "api"}})

srv := web.New(logger)
// Pre-wired stack: Recover → RequestID (UUID v7/v4) → RequestLogger → [CORS]

srv.Get("/health", health.NewHandler(logger, db, cache))

lc := launcher.New(logger)
lc.Append(srv)
lc.BeforeStart(func() error {
    srv.Mount("/v1", myRouter)
    return nil
})
lc.Run()

With origins (CORS auto-applied):

srv := web.New(logger, web.Config{
    Server:         server.Config{Port: 9090},
    AllowedOrigins: []string{"https://example.com"},
})

Environment variables for web.New:

Variable Default Effect
EINHERJAR_SERVER_HOST 0.0.0.0 Bind address
EINHERJAR_SERVER_PORT 8080 Listen port
EINHERJAR_SERVER_READ_TIMEOUT 5s HTTP read timeout
EINHERJAR_SERVER_WRITE_TIMEOUT 10s HTTP write timeout
EINHERJAR_SERVER_IDLE_TIMEOUT 120s Keep-alive idle timeout
EINHERJAR_SERVER_SHUTDOWN_TIMEOUT 10s Graceful shutdown budget
EINHERJAR_SERVER_CORS_ORIGINS (empty — CORS off) Comma-separated allowed origins
Tier 2 — Full control (server.New)

Explicit middleware composition:

import (
    "code.nochebuena.dev/einherjar/web/mw"
    "code.nochebuena.dev/einherjar/web/server"
)

srv := server.New(logger, server.Config{Port: 9090},
    server.WithMiddleware(
        mw.Recover(),
        mw.RequestID(myIDGenerator),
        mw.CORS([]string{"*"}),
        mw.RequestLogger(logger),
        myOwnMiddleware,
    ),
)

Both tiers share the same server.Config, env variables, and lifecycle.Component contract. The only difference is how much wiring is automated.


Middleware
import "code.nochebuena.dev/einherjar/web/mw"

// Rate limiting — in-memory token bucket (swap for distributed store at scale)
limiter := mw.NewInMemoryRateLimiterStore(100, 20) // rps=100, burst=20
srv.Use(mw.IPRateLimit(limiter, logger))
srv.Use(mw.UserRateLimit(limiter, logger))

// Scale: swap store without changing middleware
// valkeyLimiter := valkeymw.NewRateLimiterStore(valkey, 100, 20) // implements mw.RateLimiterStore
// srv.Use(mw.IPRateLimit(valkeyLimiter, logger))

IPRateLimit uses X-Forwarded-ForRemoteAddr as the rate-limit key. UserRateLimit uses the authenticated user ID from security.Identity; falls back to client IP when no identity is present in context.

Both middlewares fail open on store error — the request is allowed, the error is logged. This keeps the service available when the rate-limit store is degraded.


Generic handlers
import (
    "code.nochebuena.dev/einherjar/core/valid"
    "code.nochebuena.dev/einherjar/web/httputil"
)

type CreateUserReq struct {
    Email string `json:"email" validate:"required,email"`
    Name  string `json:"name"  validate:"required"`
}
type CreateUserRes struct {
    ID string `json:"id"`
}

v := valid.New()

// POST /users — decode body → validate → call service → encode response
srv.Post("/users", httputil.Handle(v, func(ctx context.Context, req CreateUserReq) (CreateUserRes, error) {
    id, err := userService.Create(ctx, req.Email, req.Name)
    if err != nil {
        return CreateUserRes{}, err
    }
    return CreateUserRes{ID: id}, nil
}))

// GET /users/{id} — no request body
srv.Get("/users/{id}", httputil.HandleNoBody(func(ctx context.Context) (CreateUserRes, error) {
    // ...
}))

// DELETE /users/{id} — no response body
srv.Delete("/users/{id}", httputil.HandleEmpty(v, func(ctx context.Context, req DeleteReq) error {
    return userService.Delete(ctx, req.ID)
}))

Validation failures return 400 with a structured JSON error. All *xerrors.Err values are mapped to their canonical HTTP status codes (full 16-code table below).


Health endpoint
import "code.nochebuena.dev/einherjar/web/health"

// db and cache implement observability.Checkable
srv.Get("/health", health.NewHandler(logger, db, cache))

// Response shape:
// {"status":"UP","components":{"db":{"status":"UP","latency":"1.2ms"}}}
// {"status":"DEGRADED","components":{"cache":{"status":"DEGRADED","latency":"50ms","error":"timeout"}}}
// {"status":"DOWN","components":{"db":{"status":"DOWN","latency":"5s","error":"connection refused"}}}

All checks run concurrently within a configurable timeout (default 5s). DOWN (critical priority failure) → HTTP 503. DEGRADED (degraded priority failure) → HTTP 200.

Environment variables:

Variable Default Effect
EINHERJAR_HEALTH_CHECK_TIMEOUT 5s Maximum time to wait for all checks

HTTP Status Code Mapping

httputil.Error maps *xerrors.Err codes to HTTP status:

Code HTTP
ErrInvalidInput, ErrOutOfRange 400
ErrUnauthorized 401
ErrPermissionDenied 403
ErrNotFound 404
ErrAlreadyExists, ErrAborted 409
ErrGone 410
ErrPreconditionFailed 412
ErrRateLimited 429
ErrCancelled 499
ErrInternal, ErrDataLoss 500
ErrNotImplemented 501
ErrUnavailable 503
ErrDeadlineExceeded 504

Dependency Graph

contracts  (zero dependencies)
    ↑
  core     (contracts)
    ↑
  web      (contracts, core, chi/v5, uuid, x/time)
    ↑
  your app

db-*, cache-*, and storage-* starters never import web — they only need contracts and core. Repositories do not know HTTP exists.


Verification

cd web/
go build ./...     # must compile clean
go vet ./...       # no warnings
go test ./...      # structural + behavioural compliance passes
gofmt -l .         # no output

The gate does not decide who passes. It decides that passing has consequences.

Documentation

Overview

Package web provides the HTTP foundation for Einherjar services.

The root package exposes New — a factory that returns a lifecycle-managed chi server with the recommended middleware stack pre-applied (request ID, panic recovery, request logging). Import the sub-packages directly for full control over individual components.

Sub-packages:

Happy path

logger := logz.New(logz.Config{JSON: true})
lc := launcher.New(logger)

srv := web.New(logger)
srv.Get("/health", health.NewHandler(logger, db, cache))

lc.Append(srv)
lc.BeforeStart(func() error {
    // register routes
    return nil
})

if err := lc.Run(); err != nil {
    logger.Error("launcher failed", err)
    os.Exit(1)
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func New

func New(logger logging.Logger, cfg ...Config) server.Server

New creates a server.Server with the recommended middleware stack pre-applied:

  1. Recover — catches panics, returns 500
  2. RequestID — injects UUID v7 request ID (falls back to v4)
  3. RequestLogger — logs method, path, status, latency
  4. CORS — applied only when cfg.AllowedOrigins is non-empty

For full control over middleware composition use server.New directly.

Types

type Config

type Config struct {
	Server         server.Config
	AllowedOrigins []string `env:"EINHERJAR_SERVER_CORS_ORIGINS" envSeparator:","`
}

Config aggregates configuration for the web module. Server holds HTTP server settings; all fields carry caarlos0/env struct tags. AllowedOrigins is programmatic-only — set it directly or via the env tag.

Directories

Path Synopsis
Package health provides a concurrent health check HTTP handler.
Package health provides a concurrent health check HTTP handler.
Package httputil provides typed handler adapters and HTTP response helpers.
Package httputil provides typed handler adapters and HTTP response helpers.
Package mw provides transport-level HTTP middleware for Einherjar services.
Package mw provides transport-level HTTP middleware for Einherjar services.
Package server provides a lifecycle-aware HTTP server for Einherjar services.
Package server provides a lifecycle-aware HTTP server for Einherjar services.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL