fibermap

package
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 13, 2026 License: MIT Imports: 35 Imported by: 0

README

fibermap

YAML-декларативный роутер и middleware-композер для Fiber v2. Описываете роуты в YAML, регистрируете хендлеры и middleware по имени (никакой рефлексии), получаете типизированный per-request контекст и отгружаете сервис через Run(), который в коробке содержит recover + request-id + slog + Prometheus + healthz.

Импорт: github.com/theizzatbek/gokit/fibermap Зависит от: gofiber/fiber/v2, gopkg.in/yaml.v3, prometheus/client_golang, github.com/theizzatbek/gokit/errs, github.com/theizzatbek/gokit/fibermap/bind

Зачем это нужно

Hand-rolled Fiber-bootstrap означает заново-решать routes-vs-code-coupling, middleware-ordering, OpenAPI-интеграцию, panic-recovery, метрики, healthcheck'и и graceful shutdown на каждом сервисе. fibermap переносит всё это в один декларативный файл (routes.yaml) + build-once-mount-once Engine[T] configurator. Три вещи пронизывают пакет:

  1. Lifecycle enforced. New[T]() → SetContextBuilder → Register* → LoadFile → Run (или Mount). Mount валидирует всё вместе и возвращает errors.Join всех проблем; ничто не устанавливается частично.
  2. Per-request Context[T] строится один раз и пропагируется. Хендлеры видят *Context[T], где T — это ваш AppCtx, несущий request-scoped data (user_id, request_id, logger). Один ContextBuilder, потом каждый хендлер читает типизированный state.
  3. Middleware-цепочки резолвятся декларативно. Группы middleware_set + per-route middleware:-списки. Plain (bearer) и factory (require_role: [admin]) формы; sets рекурсивно расширяются; дубликаты deduped'ятся по (name, args).

Quickstart

routes.yaml:

groups:
  - prefix: /v1
    routes:
      - method: GET
        path: /ping
        handler: ping
        name: ping.get

main.go:

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/theizzatbek/gokit/fibermap"
)

type AppCtx struct {
    UserID string
}

func main() {
    eng := fibermap.New[AppCtx]()
    eng.SetContextBuilder(func(c *fiber.Ctx) (AppCtx, error) {
        return AppCtx{}, nil
    })

    fibermap.RegisterHandler(eng, "ping", func(c *fibermap.Context[AppCtx]) error {
        return c.SendString("pong")
    })

    if err := eng.Run(fibermap.WithAddr(":3000")); err != nil {
        panic(err)
    }
}

Вот и всё. Бандл Run даёт вам /healthz, /metrics, request-id, структурированные access-логи и panic-recovery бесплатно. Используйте fibermap.Default[T]() вместо New[T](), чтобы также встроить ops-бандл в сам engine (авто-применяется даже в тестах через Mount).

fibermap.RegisterHandler выше принимает raw *fibermap.Context[T] и оставляет body/query/params/headers parsing на handler. Для большинства реальных endpoint'ов используйте типизированные регистры — они parsят + валидируют входной struct, авто-attach'ат схему для OpenAPI и сводят boilerplate handler'а к одной строке req argument:

type CreateTaskReq struct {
    Title    string   `json:"title"    validate:"required,min=1,max=200"`
    Tags     []string `json:"tags"`
    Priority int      `json:"priority" validate:"oneof=1 2 3"`
}

fibermap.RegisterHandlerWithBody(eng, "tasks.create",
    func(c *fibermap.Context[AppCtx], req CreateTaskReq) error {
        // req уже распарсен и провалидирован — никаких c.BodyParser, никаких errsval.
        return c.Status(201).JSON(...)
    },
    fibermap.WithResponse(201, Task{}),
)

Sibling-хелперы — RegisterHandlerWith{Query,Params,Headers} — для single-source binding. Для комбинированных кейсов (body + path-param, etc.) — RegisterHandlerWithInput (см. ниже § «Комбинированные binder'ы»). Все четыре + Input принимают eng.validator через SetValidator и роутят bind/validation failures через Engine.BindErrorFunc (default 400 JSON; override через SetBindErrorHandler).

Конфигурация

Сборка engine'а
Функция Что даёт
New[T]() *Engine[T] bare-engine; вы opt in'ите в каждую фичу
Default[T]() *Engine[T] engine с WithRecover + WithRequestLogger + WithMetrics + WithHealthCheck defaults pre-applied к Run
Методы setup engine'а
Метод Когда вызывать
SetContextBuilder(fn ContextBuilder[T]) Обязательно. Строит per-request Context[T].Data из *fiber.Ctx (читать Bearer-locals, request-id и т.д.)
SetValidator(v bind.Validator) Опционально. Используется RegisterHandlerWithBody/Query/Params/Headers для валидации декодированных struct'ов
SetCacheDefaults(d CacheDefaults[T]) Опционально. KeyBy / Headers / Control defaults для YAML-блока cache:
SetBindErrorHandler(fn BindErrorFunc[T]) Опционально. Кастомный error-mapping для bind-failures. По умолчанию — 400 JSON с {"error":"..."} (текст из err.Error()). Для production-сервисов, использующих *errs.Error-wire-shape везде, передавайте fibermap.ErrsvalBindError[T] (см. ниже) — он маппит parse/validate в {code, message, details[]} и source-aware code'ы.
Опции Run (RunOption)
Опция По умолчанию Заметки
WithAddr(":3000") из env $PORT, иначе :3000 TCP listen-адрес
WithRoutesPath("routes.yaml") routes.yaml Путь, передаваемый во внутренний LoadFile, если вы пропустили ручной LoadFile
WithRoutesFS(fs.FS) none embed.FS источник — встраивает routes.yaml в бинарь
WithFiberConfig(fiber.Config) минимальный Кастомный *fiber.App config (override ErrorHandler, BodyLimit и т.д.)
WithUse(handlers ...fiber.Handler) [RequestID] Fiber-level middleware, устанавливаемый ДО engine'овского contextInit
WithConfigureApp(fn func(*fiber.App)) none Хук для манипуляции *fiber.App после Mount
WithShutdownTimeout(d) 10s Graceful shutdown deadline на SIGINT/SIGTERM
WithoutSignalHandling() Пропустить built-in signal-handler (caller управляет shutdown'ом)
WithRecover(logger) / WithoutRecover() on (slog.Default) Panic-recovery со стек-трейсом
WithoutRequestID() request-id on Инжектит X-Request-ID
WithRequestLogger(logger, skipPaths...) / WithoutRequestLogger() on (пропускает /healthz,/metrics) Структурированный access-лог
WithMetrics(path) / WithoutMetrics() /metrics (только через Default[T]) Prometheus endpoint
WithMetricsRegistry(reg) приватный registry Route middleware + scrape через caller-provided registry — унифицирует fibermap_http_* с собственными коллекторами app'а.
WithHealthCheck(path) / WithoutHealthCheck() /healthz Always-200 health endpoint, обходит ContextBuilder
WithReadiness(path, checkers...) off Авто-probed readiness endpoint — запускает каждый Checker параллельно, 200 {"status":"ok"} или 503 {"status":"degraded","checks":{…}}. Обходит ContextBuilder.
WithReadinessOpts(opts...) none Прокидывает []ReadinessOption (например, WithReadinessTimeout(d)) в авто-установленный readiness handler.

Common patterns

YAML-схема полного роута
groups:
  - prefix: /api/v1
    middleware:                       # group-level: применяется к каждому вложенному роуту + sub-group
      - bearer: []                    # factory middleware: map-форма даже с пустыми args
    groups:                           # вложенные группы наследуют middleware
      - prefix: /tasks
        routes:
          - method: GET
            path: ""                  # пусто = сам group-prefix
            handler: tasks.list       # имя, зарегистрированное через RegisterHandler
            name: tasks.list          # обязательно: стабильное имя для OpenAPI / Routes()
            tags: [tasks]             # опционально: openapi tag(s)
            summary: List tasks
            description: List the caller's tasks
            middleware:               # route-level: добавляется ПОСЛЕ group-middleware
              - require_role: [admin]
            timeout: 5s               # опционально: per-route timeout
            cache:                    # опционально: response-cache
              ttl: 10s
              control: true           # уважать Cache-Control: no-store
              headers: true           # cache + replay response-headers
Типизированный body-bound хендлер
type CreateTaskRequest struct {
    Title string `json:"title" validate:"required,min=1,max=200"`
}

type Task struct {
    ID    string `json:"id"`
    Title string `json:"title"`
}

fibermap.RegisterHandlerWithBody(eng, "tasks.create",
    func(c *fibermap.Context[AppCtx], req CreateTaskRequest) error {
        t := svc.Create(c.Data.UserID, req.Title)
        return c.Status(201).JSON(t)
    },
    fibermap.WithResponse(201, Task{}),     // для OpenAPI
    fibermap.WithResponse(400, errs.Response{}),
)

Sibling-хелперы: RegisterHandlerWithQuery, RegisterHandlerWithParams, RegisterHandlerWithHeaders. Все прогоняют eng.validator против декодированного struct'а перед вызовом хендлера. Bind-failures всплывают как *errs.Error{Kind: Validation}, маппящееся в 400.

Комбинированные binder'ы — RegisterHandlerWithInput

Когда один эндпоинт нуждается более чем в одном из {body, params, query, headers} типизированных вместе — например, PATCH /things/:id с body, path id и query filter — используйте RegisterHandlerWithInput. Input-struct объявляет любую комбинацию полей именами строго Body, Params, Query, Headers:

type UpdateThingInput struct {
    Body   UpdateBody       // {"title": "...", "tags": [...]}
    Params struct {         // /things/:id
        ID string `params:"id" validate:"required,uuid"`
    }
    Query struct {          // ?notify=true
        Notify bool `query:"notify"`
    }
}

fibermap.RegisterHandlerWithInput(eng, "things.update",
    func(c *fibermap.Context[AppCtx], in UpdateThingInput) error {
        // in.Body, in.Params, in.Query уже распарсены + валидированы.
        return c.JSON(svc.Update(in.Params.ID, in.Body, in.Notify))
    })

Кит рефлектит Input один раз на регистрации, строит binder-список и переиспользует его per request — никакого cost'а рефлексии на hot path'е сверх field-index lookup'а на каждое recognised-поле. Поля с именами вне зарезервированного набора игнорируются.

Каждое recognised-поле авто-прикрепляет своё matching With*-опцию, так что генерация OpenAPI видит полный набор схем без того, чтобы caller пробрасывал какие-либо opts. Валидация проходит через eng.validator точно так же, как для single-source вариантов.

Misuse panic'ит на регистрации:

  • Input не struct.
  • Нет recognised-поля (используйте plain RegisterHandler).
  • recognised-поле, тип которого не struct.

С v1.1.0 fibermap.ErrsvalBindError[T] готовый BindErrorFunc[T] для сервисов, использующих *errs.Error-wire-shape повсеместно. Wire'ить один раз на engine setup:

eng.SetBindErrorHandler(fibermap.ErrsvalBindError[AppCtx])

После этого все parse/validate failures из bind.Body / Query / Params / HeaderRegisterHandlerWithInput) маппятся в kit-овский {code, message, details[]} JSON:

{
  "code": "invalid_body",
  "message": "request validation failed",
  "details": [
    {"field": "Email", "rule": "email", "param": "", "message": "Email must be a valid email"},
    {"field": "Age",   "rule": "min",   "param": "18", "message": "Age must be at least 18"}
  ]
}

Source-aware codes:

Source Code
body parse/validate invalid_body
query parse/validate invalid_query
params parse/validate invalid_params
header parse/validate invalid_header
anything else invalid_request

Когда срабатывает Details[] extraction: только когда обёрнутая ошибка — это validator.ValidationErrors (стандартный путь через go-playground/validator/v10). Parse-stage failures (битый JSON, missing-required URL-param) дают единственное validation-error с err.Error() в message-поле, без details[].

Что НЕ переопределяется: kit defaultBindError остаётся {"error":"..."}fibermap-пакет не форсит errs-конвенцию. Передавайте ErrsvalBindError[T] явно когда хотите её.

Factory middleware (параметризуемые)
// На registration-time
fibermap.RegisterMiddlewareFactory(eng, "require_role",
    func(args []string) (fibermap.MiddlewareFunc[AppCtx], error) {
        roles := args
        return func(c *fibermap.Context[AppCtx]) error {
            if !slices.Contains(roles, c.Data.Role) {
                return errs.Permission("forbidden", "missing required role")
            }
            return c.Next()
        }, nil
    })

// В routes.yaml
middleware:
  - require_role: [admin]
  - require_role: [editor, owner]   # разные args = отдельный handler в dedup-кеше
Mount на существующий *fiber.App (тесты + композируемость)
app := fiber.New(fiber.Config{
    ErrorHandler: fibermap.ErrorHandler(logger),  // подключает errs.HTTP в Fiber
})
app.Use(authObj.Bearer(auth.BearerOptional))      // pre-engine middleware
if err := eng.Mount(app); err != nil {
    return err
}
resp, _ := app.Test(httptest.NewRequest("GET", "/v1/ping", nil), -1)

Mount — это единственный способ использовать engine без Run — нужен для in-process тестов с app.Test.

Программные роуты (сырые Fiber-хендлеры)
eng.Add("POST", "/auth/refresh", "auth.refresh",
    func(c *fibermap.Context[AppCtx]) error {
        return authObj.IssueRefresh(c.Ctx)  // оборачивает сырой *fiber.Ctx хендлер
    })

Программные роуты участвуют в генерации OpenAPI и engine-овой цепочке ContextBuilder + middleware. Они не могут нести YAML-middleware (используйте WithUse или wrap руками).

Error-модель

Каждая ошибка, возвращаемая библиотекой — *fibermap.Error (alias вокруг собственного типизированного error-типа пакета) со Stage (parse / mount / register) и Code*-константой. Новые error-условия добавляют Code*-константу. Mount-stage ошибки аккумулируются в один errors.Join, так что все проблемы всплывают в одном вызове.

Используйте fibermap.ErrorHandler(logger) как fiber.Config.ErrorHandler, чтобы подключить errs.HTTP для handler-ошибок и fallback'нуться на собственный код *fiber.Error для router-level (404/405) ошибок. Авто-логирует 5xx через переданный логгер; 4xx silent по умолчанию.

Observability

fibermap.Default[T]() (или Run с matching-опциями) shipping'ит:

  • slog access-лог с method, path, status, duration_ms, request_id, response_size
  • Prometheus метрики на /metricshttp_requests_total{method,path,status}, http_request_duration_seconds, in-flight gauge
  • Health endpoint на /healthz — обходит ContextBuilder, так что работает, даже когда auth/db лежит
  • Readiness endpoint (опционально через WithReadiness) — запускает переданные Checker'ы параллельно под shared deadline'ом; 200 {"status":"ok"} когда каждая проверка проходит, 503 {"status":"degraded","checks":{name:err}} иначе. db, clients/nats, clients/redis shipping'ят NewChecker(client, name) адаптеры.
  • Request ID пропагируется как X-Request-ID header + хранится в c.Locals(fibermap.LocalsRequestID)

Передайте *slog.Logger в WithRecover, WithRequestLogger. nil = slog.Default().

Idempotency-Key

fibermap.IdempotencyKey(store, opts...) возвращает Stripe-style idempotency-middleware: для unsafe-методов (POST/PUT/PATCH/DELETE) первый ответ, keyed by X-Idempotency-Key, capture'ится в pluggable IdempotencyStore и replay'ится дословно на последующие запросы с тем же ключом. Replay'и несут X-Idempotent-Replay: true, чтобы клиенты (и APM-трассы) могли их отличать.

store := cache.NewIdempotencyStore(svc.Redis, "idem:payments:")
app.Post("/payments",
    fibermap.IdempotencyKey(store,
        fibermap.WithIdempotencyTTL(48*time.Hour),
        fibermap.WithIdempotencyRequired(),
    ), createPayment)
Опция По умолчанию Заметки
WithIdempotencyHeader(name) X-Idempotency-Key Кастомный inbound-header.
WithIdempotencyTTL(d) 24h Сколько replay'и остаются доступными.
WithIdempotencyMethods(...) POST/PUT/PATCH/DELETE Сузить / расширить кешируемый method-set.
WithIdempotencyMaxBodySize(n) 1 MiB Oversize-ответы проходят без кеширования.
WithIdempotencyRequired() off Missing header возвращает 400 с idempotency_key_missing.
WithIdempotencySkipStatus(...) 5xx Status-коды, которые НЕ должны кешироваться.
WithIdempotencyLockTTL(d) 30s TTL SETNX-лока вокруг in-flight-хендлера (когда store реализует IdempotencyLocker).
WithIdempotencyWithoutLock() off Подавить lock-path даже если store реализует IdempotencyLocker.

Default cache-backend — clients/cache.NewIdempotencyStore(svc.Redis, prefix), который реализует и IdempotencyStore, и IdempotencyLocker (SETNX). YAML-роуты подключают factory через auth/fibermount.MountIdempotencyKeyFactory(eng, store) и используют idempotency_key: ["1h", "required"] в routes.yaml.

Concurrency-lock

Когда store реализует IdempotencyLocker (например, cache.RedisIdempotencyStore), middleware автоматически берёт короткоживущий SETNX-лок на in-flight-хендлер:

  1. На cache-miss → AcquireLock(ctx, key, lockTTL).
  2. Если lock уже занят другим request'ом → 409 Conflict с Code idempotency_in_flight.
  3. После завершения хендлера → ReleaseLock (через defer, даже на error).

Lock-TTL (default 30s) защищает от зависшего/упавшего хендлера — лок expires автоматически. Подавить через WithIdempotencyWithoutLock() если concurrent-409 шум перевешивает risk дублирующего handler-run'а (например, для очень долгих хендлеров).

Stores, которые НЕ реализуют locker, сохраняют pre-lock-поведение — два concurrent-запроса с одним ключом могут оба запустить хендлер, downstream-идемпотентность (DB unique constraints / transactional outbox) обязательна.

Request-scoped logger

fibermap.LoggerInjector(base) — это Fiber-middleware, который выводит per-request *slog.Logger из base и хранит его под LocalsLogger. Хендлеры читают его обратно через fibermap.LoggerFrom(c) и получают логгер pre-bound с:

  • method (HTTP-метод)
  • path (routed-pattern)
  • request_id (когда middleware RequestID запускался ранее)
  • user_id (когда LocalsAuthSubject populate'д — пакет service кита авто-подключает это из JWT-principal'а)
  • route (когда у роута установлен .Name(...))
app.Use(fibermap.RequestID())
app.Use(fibermap.LoggerInjector(svc.Logger()))

app.Post("/links", func(c *fiber.Ctx) error {
    fibermap.LoggerFrom(c).Info("link created", "code", code)
    // эмитит {... method=POST path=/links request_id=... user_id=... code=...}
    return c.SendStatus(201)
}).Name("links.create")

service.New авто-устанавливает LoggerInjector на уровне App; opt out через service.WithoutLoggerInjector(), когда вы подключаете свой.

LoggerFrom(nil) и LoggerFrom(c) без установленного middleware оба fallback'ятся на slog.Default(), так что handler-код остаётся panic-free в любом контексте проводки.

Security headers

fibermap.SecurityHeaders(opts...) возвращает Fiber-middleware, который добавляет OWASP-baseline response-headers — Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin и API-friendly Content-Security-Policy. Mount на уровне App, чтобы /metrics, /healthz, /readyz тоже несли headers:

app.Use(fibermap.SecurityHeaders(
    fibermap.WithHSTSIncludeSubdomains(),
    fibermap.WithCSP("default-src 'self'; script-src 'self'"),
))
Опция Эффект
WithHSTSMaxAge(seconds) Override default 1-year max-age
WithHSTSIncludeSubdomains() Append includeSubDomains — каждый sub-domain становится HTTPS-only
WithHSTSPreload() Append preload (валидно только с includeSubdomains + после регистрации в hstspreload.org)
WithoutHSTS() Дропнуть HSTS-header (для non-HTTPS деплоев)
WithCSP(policy) Override default API-friendly CSP
WithoutCSP() Дропнуть CSP-header
WithFrameOptions(value) Override X-Frame-Options (default DENY)
WithReferrerPolicy(value) Override Referrer-Policy (default strict-origin-when-cross-origin)

service.New авто-устанавливает middleware с defaults; передайте service.WithoutSecurityHeaders или service.WithSecurityHeaders(opts...), чтобы отключить или кастомизировать.

App-level middleware bundle

Под Run() встроены опт-ин middleware'ы. Каждый отдельный RunOption:

eng.Run(
    fibermap.WithCORS(),                                          // any origin, common methods/headers
    fibermap.WithRateLimit(50, 100, "/healthz", "/readyz"),       // 50 rps, burst 100, IP-keyed
    fibermap.WithBodyLimit(10 << 20),                             // 10 MiB cap → 413
    fibermap.WithCompression(fibermap.CompressionBestSpeed),      // gzip/deflate
    fibermap.WithTrustedProxies("10.0.0.0/8", "192.168.0.0/16"),  // c.IP() respects X-Forwarded-For
    fibermap.WithReqLogSlowThresholdOption(500 * time.Millisecond),
    fibermap.WithNotFoundHandler(fibermap.NotFoundJSON()),
)
Опция Эффект
WithCORS(cfg...) Установить CORS-middleware (kit defaults: any origin, common methods/headers). Конфиг — CORSConfig{AllowOrigins, AllowMethods, AllowHeaders, AllowCredentials, MaxAge}.
WithRateLimit(rps, burst, skipPaths...) In-process IP-keyed token-bucket. /healthz / /readyz / /metrics skipped by default. Для multi-replica — используйте Redis-backed limiter через WithUse.
WithBodyLimit(maxBytes) Body > maxBytes → 413. Устанавливает fiber.Config.BodyLimit (fail в parser ДО handler'а).
WithCompression(level...) gzip/deflate response compression based on Accept-Encoding. Default CompressionBestSpeed.
WithTrustedProxies(cidrs...) Enable fiber.Config.EnableTrustedProxyCheck + TrustedProxies so c.IP() returns X-Forwarded-For only от trusted hops. Production-must при работе за load balancer.
WithReqLogSlowThresholdOption(d) RequestLogger: latency < d → Debug, ≥ d → Warn, 5xx → Error always. Default = legacy Info на всех.
WithNotFoundHandler(h) Catch-all 404. Kit ships NotFoundJSON() для kit-style JSON-shape {code, message, path}.

CORS / Compression устанавливаются BEFORE rate limit / auth — preflight OPTIONS short-circuits без engagement chain'а.

Тестирование

Используйте fibermap/fibermaptest для assertion'ов над Engine.Routes() (route-inventory проверки). Для request-level тестов используйте Engine.Mount(app) на свежем *fiber.App и драйвите app.Test(req).

func TestRoutes(t *testing.T) {
    eng := buildEngine(t)                    // ваш setup
    app := fiber.New(fiber.Config{ErrorHandler: fibermap.ErrorHandler(nil)})
    if err := eng.Mount(app); err != nil { t.Fatal(err) }

    resp, _ := app.Test(httptest.NewRequest("GET", "/v1/ping", nil), -1)
    require.Equal(t, 200, resp.StatusCode)
}

Ограничения

  • Нет built-in rate-limiting'а. Используйте gofiber/fiber/v2/middleware/limiter через WithUse.
  • Нет hot-reload routes.yaml. Грузится один раз на старте.
  • Нет per-route auth декларативного shorthand'а. Используйте middleware-factory, зарегистрированные через auth/fibermount.MountMiddlewareFactories.
  • YAML-ошибки на parse-time, не edit-time. Используйте routes.schema.json (см. schemas/) в вашем редакторе для live-валидации.
  • Mount/Run можно вызвать только один раз на engine. Re-mount — это программерская ошибка (panic'ит).

См. также

  • fibermap/bind — декодирование request body/query/header/params + валидация
  • fibermap/factory — хелперы для сборки middleware-factory
  • fibermap/fibermaptest — testing-хелперы для inventory Routes()
  • fibermap/openapi — генерация OpenAPI 3.0 spec'а из Engine.Routes()
  • schemas/ — JSON-schema'ы для всех YAML-конфигов кита (routes/clients/crons/natsmap)
  • auth/fibermount — монтирует bearer/require_scope/require_role factory на engine
  • errs — typed-error контракт, используемый ErrorHandler'ом
  • examples/urlshort/ — полный интеграционный пример

Documentation

Overview

Package fibermap loads YAML-described HTTP route trees and middleware chains into a Fiber router. It exposes a generic per-request context type so handlers can read pre-built typed data without re-parsing locals.

Lifecycle: New → SetContextBuilder → RegisterHandler / RegisterMiddleware / RegisterMiddlewareFactory → LoadFile/LoadBytes → Mount.

See the package README and design spec for details.

Example

Example shows the full lifecycle: configure the engine, register a handler and a middleware, load YAML from memory, mount onto a Fiber router, and introspect the registered routes.

package main

import (
	"fmt"

	"github.com/gofiber/fiber/v2"
	"github.com/theizzatbek/gokit/fibermap"
)

// docCtx is the per-request payload used by the examples below.
type docCtx struct {
	UserID string
	Role   string
}

// Alias example shows the recommended way to hide the generic parameter
// from handler signatures in user code.
type docCtxRef = fibermap.Context[docCtx]

func main() {
	eng := fibermap.New[docCtx]()

	eng.SetContextBuilder(func(c *fiber.Ctx) (docCtx, error) {
		return docCtx{UserID: "u-1", Role: "guest"}, nil
	})

	eng.RegisterMiddleware("auth", func(c *docCtxRef) error {
		return c.Next()
	})
	eng.RegisterHandler("ping", func(c *docCtxRef) error {
		return c.SendString("pong " + c.Data.UserID)
	})

	if err := eng.LoadBytes([]byte(`
groups:
  - prefix: /v1
    middleware: [auth]
    routes:
      - { method: GET, path: /ping, handler: ping }
`)); err != nil {
		panic(err)
	}
	if err := eng.Mount(fiber.New()); err != nil {
		panic(err)
	}

	fmt.Println(len(eng.Routes()), "route(s)")
}
Output:
1 route(s)

Index

Examples

Constants

View Source
const (
	// parse stage
	CodeInvalidYAML       = "invalid_yaml"
	CodeFileNotFound      = "file_not_found"
	CodeMissingField      = "missing_field"
	CodeInvalidHTTPMethod = "invalid_http_method"
	CodeMiddlewareCycle   = "middleware_cycle"
	CodeInvalidTimeout    = "invalid_timeout"
	CodeInvalidCache      = "invalid_cache"

	// mount stage
	CodeUnknownHandler        = "unknown_handler"
	CodeUnknownMiddleware     = "unknown_middleware"
	CodeUnknownMiddlewareSet  = "unknown_middleware_set"
	CodeDuplicateRoute        = "duplicate_route"
	CodeMissingContextBuilder = "missing_context_builder"
	CodeInvalidFactoryArgs    = "invalid_factory_args"
	CodeAlreadyMounted        = "already_mounted"

	// register stage
	CodeDuplicateRegistration = "duplicate_registration"
	CodeRegisterAfterMount    = "register_after_mount"
	CodeRegisterMisuse        = "register_misuse"
)

Error codes.

View Source
const (
	// CodeIdempotencyKeyMissing — handler enforced via
	// [WithIdempotencyRequired] but the inbound request did not
	// carry the configured header.
	CodeIdempotencyKeyMissing = "idempotency_key_missing"

	// CodeIdempotencyInFlight — another request with the same
	// idempotency-key is currently executing. The store implements
	// [IdempotencyLocker] and refused the SETNX-style lock. Caller
	// should retry after a short backoff. Mapped to 409 by
	// errs.HTTP.
	CodeIdempotencyInFlight = "idempotency_in_flight"
)

Stable error Code constants returned by IdempotencyKey.

View Source
const (
	SourceYAML         = "yaml"
	SourceProgrammatic = "programmatic"
)

Route source constants used in RouteInfo.Source.

View Source
const HeaderRequestID = reqctx.HeaderRequestID

HeaderRequestID is the HTTP header RequestID reads from and writes to. Aliased to reqctx.HeaderRequestID — single source of truth across the kit.

View Source
const LocalsAuthSubject = "fibermap_auth_subject"

LocalsAuthSubject is the conventional key under which the kit's auth middleware stores the authenticated subject string. Read by LoggerFrom when no other identifier is present.

View Source
const LocalsLogger = "fibermap_request_logger"

LocalsLogger is the key under which RequestLogger stores the request-scoped *slog.Logger. Exported so subsystems that already hold a *fiber.Ctx can bypass LoggerFrom and read the slot directly.

View Source
const LocalsRequestID = "request_id"

LocalsRequestID is the Fiber Locals key under which RequestID stores the per-request identifier. Read it from a ContextBuilder via `c.Locals(fibermap.LocalsRequestID).(string)`.

Variables

View Source
var ErrStopWalk = errors.New("fibermap: stop walk")

ErrStopWalk may be returned by the function passed to Engine.Walk to stop iteration without surfacing an error to the caller.

Functions

func BodyLimit

func BodyLimit(maxBytes int) fiber.Handler

BodyLimit returns a Fiber middleware that rejects request bodies larger than maxBytes with 413 Request Entity Too Large.

NOTE: Fiber's recommended way to limit body size is the fiber.Config.BodyLimit field, which fails fast inside the parser. This middleware is the secondary check for cases where you set BodyLimit globally to a high cap but want a stricter cap on a specific subtree.

func CORS

func CORS(cfg ...CORSConfig) fiber.Handler

CORS returns a Fiber middleware enforcing CORS headers per cfg. Wraps fiber/middleware/cors with kit defaults.

func Compression

func Compression(level CompressionLevel) fiber.Handler

Compression returns a Fiber middleware that compresses responses (gzip/deflate/brotli) based on the Accept-Encoding request header.

func ErrorHandler

func ErrorHandler(logger *slog.Logger) fiber.ErrorHandler

ErrorHandler returns a fiber.ErrorHandler that converts errors to JSON responses via errs.HTTP, falls back to *fiber.Error's own Code for the router-level errors (404, 405, ...), and auto-logs server-side failures (Kind >= 500 status) via the passed logger. 4xx kinds are NOT logged — those are normal client traffic.

If logger is nil, slog.Default() is used.

func ErrsvalBindError added in v1.1.0

func ErrsvalBindError[T any](c *Context[T], err error) error

ErrsvalBindError is the recommended Engine.SetBindErrorHandler value for services that use the kit's *errs.Error contract for HTTP failure modes. It maps every parse / validate failure produced by bind.Body / Query / Params / Header (and by RegisterHandlerWithInput) into the kit's standard `{code, message, details[]}` JSON wire shape:

eng.SetBindErrorHandler(fibermap.ErrsvalBindError[AppCtx])

The helper is a no-op when err is nil. Otherwise it:

  1. Picks a source-aware Code by walking the bind sentinel chain (errors.Is against bind.ErrParseBody / ErrValidateBody / …): "invalid_body" / "invalid_query" / "invalid_params" / "invalid_header". Anything that does not match the four sources falls through to "invalid_request".

  2. Calls errsval.FromValidator to recover per-field errs.FieldError details when the wrapped error is a `validator.ValidationErrors` chain (which is what the kit's bind helpers preserve since v1.0.1 via the errors.Join wrap). If extraction succeeds the resulting *errs.Error is cloned with the source-specific Code; otherwise the helper falls back to a single-field validation error built from `err.Error()`.

  3. Renders the *errs.Error to status + JSON via errs.HTTP and writes both via *fibermap.Context (which embeds *fiber.Ctx).

Default-handler vs ErrsvalBindError trade-off

The kit ships a plain default (`{"error": "<message>"}` 400) so the `fibermap` package stays consumable without the caller buying into the errs / errsval convention. Production services that use the kit's typed errors elsewhere should call SetBindErrorHandler with this helper at engine-construction time — once — and forget it.

Behaviour for non-bind errors

Anything that doesn't match a bind sentinel (e.g. a custom handler returning a non-bind error through the same channel) is mapped to "invalid_request" with the raw `err.Error()` text. Callers can still chain a custom handler in front of ErrsvalBindError if they need different routing for a specific source error.

func IdempotencyKey

func IdempotencyKey(store IdempotencyStore, opts ...IdempotencyOption) fiber.Handler

IdempotencyKey returns a Fiber middleware that captures the first response keyed by an inbound idempotency-key header and replays the captured shape on every subsequent hit with the same key (within the TTL).

Replays carry an `X-Idempotent-Replay: true` response header so clients (and operators inspecting traces) can distinguish them from a fresh handler run.

app.Post("/payments", fibermap.IdempotencyKey(store,
    fibermap.WithIdempotencyTTL(48*time.Hour),
    fibermap.WithIdempotencyRequired(),
), createPayment)

Default-on behaviour:

  • Header is `X-Idempotency-Key`.
  • Replays apply to POST / PUT / PATCH / DELETE only.
  • TTL is 24h.
  • Response bodies > 1 MiB pass through uncached.
  • 5xx responses are NOT cached (transient — let the next attempt re-evaluate).
  • Missing header → pass through (override with WithIdempotencyRequired).

Concurrency note: two simultaneous requests with the same key may BOTH run the handler — the middleware does not lock around the underlying store. Downstream systems must be idempotent themselves (the kit's transactional outbox plus DB unique constraints is the canonical pattern). The middleware suppresses DUPLICATE work across NON-overlapping requests, not concurrent ones.

func Lint

func Lint(data []byte) error

Lint runs schema-level validation on a routes.yaml document: required fields, valid HTTP methods, middleware_set cycle detection, and middleware-entry shape. It does NOT verify that referenced handler, middleware, or factory names are registered — for that, use Engine.Validate() after registering everything.

Intended for CI tools and pre-commit hooks that want to flag bad YAML without instantiating an Engine.

func LintFile

func LintFile(path string) error

LintFile is Lint over the contents of a file. File-not-found is surfaced as *Error / CodeFileNotFound.

func LoggerFrom

func LoggerFrom(c *fiber.Ctx) *slog.Logger

LoggerFrom returns the request-scoped logger installed by RequestLogger, enriched with the subject from the kit's auth principal at call time when available. Falls back to slog.Default when no per-request logger is present (e.g. the middleware wasn't installed, or the call site is outside the request lifecycle).

Subject discovery order:

  1. c.Locals(LocalsAuthSubject) — wire this from your auth middleware once and every LoggerFrom call inherits it.
  2. fall through with no user_id attr.

Route name discovery: c.Route().Name when set (programmatic routes / YAML routes both populate this). Suppressed when empty to keep log lines clean.

func LoggerInjector

func LoggerInjector(base *slog.Logger) fiber.Handler

LoggerInjector returns a middleware that derives a request-scoped *slog.Logger from base and stores it under LocalsLogger. Every log statement made via LoggerFrom in downstream handlers automatically carries:

  • method
  • path (routed pattern when matched, raw URI otherwise)
  • request_id (when RequestID middleware populated it)

Other attrs (user_id, route_name) are NOT bound at install time because they may not exist yet — they're added lazily inside LoggerFrom from whatever Locals state is present at the call site.

nil base falls back to slog.Default at logger construction time.

Distinct from RequestLogger: that one EMITS access logs at request end; this one ATTACHES a logger handlers can use mid- request.

app.Use(fibermap.RequestID())
app.Use(fibermap.LoggerInjector(svc.Logger()))
// later, in a handler:
logger := fibermap.LoggerFrom(c)
logger.Info("created link", "code", code)
// emits {... method=POST path=/links request_id=... user_id=... code=...}

func Metrics

func Metrics() (fiber.Handler, *prometheus.Registry)

Metrics returns a Fiber middleware that records Prometheus metrics for every request, plus the *prometheus.Registry the metrics live on. Three series are exported:

  • `fibermap_http_requests_total{method,route,status}` — counter
  • `fibermap_http_request_duration_seconds{method,route,status}` — histogram (default buckets)
  • `fibermap_http_requests_in_flight` — gauge

`route` is the Fiber route template (e.g. `/v1/tasks/:id`), not the concrete path — keeps label cardinality bounded. Requests that don't match any registered route get an empty `route` label.

Pair with MetricsHandler to expose the values at a scrape endpoint, or use WithMetrics to wire both in one call:

mw, reg := fibermap.Metrics()
app.Use(mw)
app.Get("/metrics", fibermap.MetricsHandler(reg))

The returned registry is private — only fibermap HTTP metrics are registered on it. To unify the kit's per-subsystem metrics (db, httpc, nats, …) under one scrape endpoint, use MetricsOn with your own *prometheus.Registry instead, or pass that registry to Run via WithMetricsRegistry.

func MetricsHandler

func MetricsHandler(reg *prometheus.Registry) fiber.Handler

MetricsHandler returns a fiber.Handler that exposes `reg`'s metrics in Prometheus text format. Mount it under your scrape path — typically `/metrics`:

app.Get("/metrics", fibermap.MetricsHandler(reg))

func MetricsHandlerFor

func MetricsHandlerFor(g prometheus.Gatherer) fiber.Handler

MetricsHandlerFor is the Gatherer-typed variant of MetricsHandler. Use when the unified registry is held as a prometheus.Gatherer rather than the concrete *prometheus.Registry — e.g. the registry constructed by [service.Service].

func MetricsOn

func MetricsOn(reg prometheus.Registerer) fiber.Handler

MetricsOn is like Metrics but registers the request counter, duration histogram, and in-flight gauge on a caller-provided Registerer. This lets a single Prometheus registry collect both fibermap HTTP metrics and other subsystem metrics (db, httpc, nats, …) so a single `/metrics` scrape endpoint exposes all of them.

reg := prometheus.NewRegistry()
app.Use(fibermap.MetricsOn(reg))
app.Get("/metrics", fibermap.MetricsHandlerFor(reg))

Panics with prometheus.AlreadyRegisteredError if MetricsOn is called twice on the same Registerer (collectors are unique per registry).

func NotFoundJSON

func NotFoundJSON() fiber.Handler

NotFoundJSON returns a route handler that emits a JSON 404 body matching the kit's errs.HTTP shape:

{ "code": "not_found", "message": "no route matched", "path": "..." }

Suitable for both 404 (no route matched) and 405 (method not allowed) catch-alls. Install via WithNotFoundHandler in Run, or directly on the fiber.App via app.Use at the END of the chain.

func RateLimit

func RateLimit(cfg RateLimitConfig) fiber.Handler

RateLimit returns a Fiber middleware enforcing a per-key request budget over a rolling window. Wraps fiber/middleware/limiter with kit-friendly skip-path handling — /healthz / /readyz / /metrics are almost always called by k8s probes and should bypass user-facing rate limits.

Backed by go-redis/in-memory limiter; pass a Storage in fiber.Config or via a custom KeyGenerator+wrapper for distributed limits. The kit ships only the in-process baseline.

func Readiness

func Readiness(checkers []Checker, opts ...ReadinessOption) fiber.Handler

Readiness returns a fiber.Handler that runs every checker concurrently under one shared deadline (default 5s) and translates the outcome into:

  • 200 OK with body `{"status":"ok"}` if every check passed.
  • 503 Service Unavailable with body `{"status":"degraded","checks":{"<name>":"<error>"}}` if any check failed (or its context deadline expired). The map only carries failed checks — successful ones are implied.

The handler is route-handler shape (no c.Next call) so it bypasses the Use chain — same convention as the /healthz built-in. Wire it into Run via WithReadiness. Zero-checker calls return the "all-ok" body so a Service that hasn't wired any subsystem still passes the probe.

func Recover

func Recover(logger *slog.Logger) fiber.Handler

Recover returns a Fiber middleware that traps panics in downstream handlers and converts them to a 500 response. It wraps Fiber's stock `middleware/recover` with stack traces enabled and a `*slog.Logger`-aware stack handler.

If `logger` is nil, panic + stack go to the default slog logger (slog.Default). The log includes the request method, path, and request_id (when RequestID populated it).

Install as early as possible — typically first in the chain so it catches panics in any other middleware too. The `WithRecover` `RunOption` does this for you.

eng.Run(fibermap.WithRecover(logger), ...)

Or directly:

app.Use(fibermap.Recover(logger))

func RegisterHandler

func RegisterHandler[T any](e *Engine[T], name string, h HandlerFunc[T], opts ...HandlerOption)

RegisterHandler is the package-level form of Engine.RegisterHandler. It exists so callers can use a uniform `fibermap.Register*` style across plain handlers, body-binding handlers, middleware, and factories (the body-binding helpers have to be package-level functions because Go does not allow generic methods, so package-level is the lowest common surface).

Behaviour and panic semantics are identical to the method form.

fibermap.RegisterHandler(eng, "tasks.list", taskH.List,
    fibermap.WithResponse(200, ListResp{}),
)

func RegisterHandlerWithBody

func RegisterHandlerWithBody[T, Req any](e *Engine[T], name string, h func(*Context[T], Req) error, opts ...HandlerOption)

RegisterHandlerWithBody registers a typed-body handler. The wrapper parses the request body into Req via bind.Body (using the engine's validator if set), then invokes h with the populated value.

One source of truth for the request type: it appears in h's signature and is automatically attached as a schema for OpenAPI generation. No separate bind.Body call inside the handler, no extra WithBody option.

type CreateTaskReq struct {
    Title string `json:"title" validate:"required,min=1,max=200"`
}

func (h *Handler) Create(c *Ctx, req CreateTaskReq) error {
    // req is already parsed + validated.
    return c.Status(201).JSON(...)
}

fibermap.RegisterHandlerWithBody(eng, "tasks.create", h.Create,
    fibermap.WithResponse(201, Task{}),
)

Parse / validate failures are routed through the engine's BindErrorFunc (default 400 JSON; customise via Engine.SetBindErrorHandler).

Extra HandlerOption values (WithResponse, WithQuery, WithHeaders, …) are forwarded to RegisterHandler — only the request-body schema is auto-derived from Req.

func RegisterHandlerWithHeaders

func RegisterHandlerWithHeaders[T, Req any](e *Engine[T], name string, h func(*Context[T], Req) error, opts ...HandlerOption)

RegisterHandlerWithHeaders is the header analogue of RegisterHandlerWithBody. Fields on Req use the `reqHeader:` tag.

func RegisterHandlerWithInput

func RegisterHandlerWithInput[T, Input any](e *Engine[T], name string, h func(*Context[T], Input) error, opts ...HandlerOption)

RegisterHandlerWithInput is the multi-source binder registrar. Use it when one endpoint needs more than one of {body, route params, query, headers} typed and validated — i.e. cases the single-source RegisterHandlerWith{Body,Params,Query,Headers} variants don't cover.

Input must be a struct whose fields are themselves structs, named (case-sensitive) from the reserved set:

Body    — parsed via Fiber's BodyParser   (json/form/multipart tags)
Params  — parsed via Fiber's ParamsParser (`params:` tag)
Query   — parsed via Fiber's QueryParser  (`query:` tag)
Headers — parsed via Fiber's ReqHeaderParser (`reqHeader:` tag)

Any combination is allowed; fields with other names are ignored. Example:

type UpdateInput struct {
    Body   UpdateBody     // {"title":..., "tags":[...]}
    Params struct {       // /tasks/:id
        ID string `params:"id" validate:"required,uuid"`
    }
    Query struct {        // ?notify=true
        Notify bool `query:"notify"`
    }
}

fibermap.RegisterHandlerWithInput(eng, "tasks.update",
    func(c *fibermap.Context[T], in UpdateInput) error {
        // in.Body, in.Params, in.Query already parsed + validated.
    })

Validation uses the engine's validator (SetValidator) for every recognised field, mirroring the single-source registrars. Bind / validation errors route through Engine.BindErrorFunc (same default 400 JSON, same SetBindErrorHandler hook).

Each recognised field also auto-attaches the matching HandlerOption (WithBody / WithParams / WithQuery / WithHeaders) so OpenAPI generation sees the full schema set without the caller threading any opts.

Panics with *Error{CodeRegisterMisuse} on misuse: Input is not a struct, no field matches the reserved set, or a matched field is not itself a struct.

func RegisterHandlerWithParams

func RegisterHandlerWithParams[T, Req any](e *Engine[T], name string, h func(*Context[T], Req) error, opts ...HandlerOption)

RegisterHandlerWithParams is the route-param analogue of RegisterHandlerWithBody. Fields on Req use the `params:` tag. The Req schema is auto-attached via WithParams so OpenAPI generation picks up its validate-derived constraints.

func RegisterHandlerWithQuery

func RegisterHandlerWithQuery[T, Req any](e *Engine[T], name string, h func(*Context[T], Req) error, opts ...HandlerOption)

RegisterHandlerWithQuery is the query-string analogue of RegisterHandlerWithBody. Fields on Req use the `query:` tag.

func RegisterMiddleware

func RegisterMiddleware[T any](e *Engine[T], name string, m MiddlewareFunc[T])

RegisterMiddleware is the package-level form of Engine.RegisterMiddleware.

func RegisterMiddlewareFactory

func RegisterMiddlewareFactory[T any](e *Engine[T], name string, f MiddlewareFactoryFunc[T])

RegisterMiddlewareFactory is the package-level form of Engine.RegisterMiddlewareFactory.

func RequestID

func RequestID() fiber.Handler

RequestID returns a Fiber-level middleware that ensures every request carries an `X-Request-ID`:

  1. If the incoming request has an `X-Request-ID` header, use it as-is.
  2. Otherwise generate a fresh 16-hex-character identifier from crypto/rand.
  3. Stash the value on `c.Locals(fibermap.LocalsRequestID)` so the engine's ContextBuilder can read it without re-parsing the header.
  4. Echo the value back as the response `X-Request-ID` header so callers can correlate logs.
  5. Inject the value into `c.UserContext()` via reqctx.WithRequestID so downstream context-aware code (slog with reqctx.SlogHandler, httpc outbound calls, natsmap publish) carries it automatically.

Install via Engine.Run with WithUse, BEFORE any auth middleware — the ID should appear in auth-failure logs too:

eng.Run(fibermap.WithUse(fibermap.RequestID(), auth.Bearer()))

Or via plain fiber:

app.Use(fibermap.RequestID())

For different header/key conventions, copy the body of this function and adjust.

func RequestLogger

func RequestLogger(logger *slog.Logger, skipPaths ...string) fiber.Handler

RequestLogger returns a Fiber middleware that logs every request with structured fields. Backward-compatible signature; see RequestLoggerWithOptions for the option-driven form (slow-request threshold, etc.).

Fields:

  • method — HTTP method
  • path — request path (no query string)
  • status — response status code
  • latency_ms — handler latency in milliseconds
  • bytes — response body size
  • request_id — value at c.Locals(LocalsRequestID), if any
  • ip — client IP

Level: INFO for `status < 500`, ERROR otherwise. Pass nil for slog.Default(). `skipPaths` opts certain paths out — typically `/healthz` and `/metrics`.

func RequestLoggerWithOptions

func RequestLoggerWithOptions(logger *slog.Logger, opts ...RequestLoggerOption) fiber.Handler

RequestLoggerWithOptions is the option-driven variant of RequestLogger. Pass WithReqLogSlowThreshold to enable the slow / fast level split:

  • latency < threshold → Debug
  • latency >= threshold → Warn
  • status >= 500 → Error (always)

func Schema

func Schema() []byte

Schema returns the JSON Schema (draft-07) for routes.yaml as a byte slice. Useful for:

  • shipping the schema to your IDE: add the modeline `# yaml-language-server: $schema=https://raw.githubusercontent.com/theizzatbek/gokit/main/schemas/routes.schema.json` to the top of your routes.yaml and editors with the YAML language server (VS Code, GoLand, Vim with coc-yaml, ...) give you autocomplete, hover docs, and inline diagnostics;

  • serving the schema from your own admin endpoint;

  • the bundled `fibermap dump-schema` CLI command.

Source: schemas.Routes. The returned slice is a reference to the embedded copy — do not mutate it. Take a copy if you need to.

func SecurityHeaders

func SecurityHeaders(opts ...SecurityHeadersOption) fiber.Handler

SecurityHeaders returns a Fiber middleware that adds the OWASP baseline response headers:

  • Strict-Transport-Security (HSTS, 1 year max-age)
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: strict-origin-when-cross-origin
  • Content-Security-Policy (API-friendly default)

Every header is opt-out individually via the matching With* option. Headers are written via fiber.Ctx.Set in c.Next()'s hook so they survive even on error responses (mounted before the user error handler emits the body).

Mount at the App level so /metrics, /healthz, /readyz also carry the headers:

app.Use(fibermap.SecurityHeaders())

service.New auto-installs this by default; pass service.WithoutSecurityHeaders to disable, or service.WithSecurityHeaders(opts...) to override the defaults.

Types

type AddOpts

type AddOpts struct {
	Description string
	Tags        []string
}

AddOpts collects the optional metadata for Engine.Add. Pass at most one to keep call sites readable:

eng.Add("GET", "/debug/pprof/heap", "debug.heap", pprofHeap,
    fibermap.AddOpts{Tags: []string{"debug", "ops"}})

type BindErrorFunc

type BindErrorFunc[T any] func(c *Context[T], err error) error

Engine is the build-once-mount-once configurator. It is parameterized by T, the per-request payload type produced by ContextBuilder. BindErrorFunc renders a bind (parse / validate) error as an HTTP response. Returned from Engine.SetBindErrorHandler; the default emits a 400 with `{"error": err.Error()}`. Inspect specific failure modes via errors.Is against bind.ErrParse* / ErrValidate* sentinels.

type CORSConfig

type CORSConfig struct {
	// AllowOrigins is the comma-separated allowlist or "*" for any
	// origin. Default "*".
	AllowOrigins string

	// AllowMethods comma-separated. Default "GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD".
	AllowMethods string

	// AllowHeaders comma-separated. Default
	// "Origin,Content-Type,Accept,Authorization,X-Request-ID".
	AllowHeaders string

	// ExposeHeaders comma-separated. Default empty.
	ExposeHeaders string

	// AllowCredentials enables Access-Control-Allow-Credentials. When
	// true, AllowOrigins MUST NOT be "*" per the CORS spec; the kit
	// does not enforce this — operator responsibility.
	AllowCredentials bool

	// MaxAge caches the preflight response. Default 12h (43200s).
	MaxAge time.Duration
}

CORSConfig configures CORS. Mirrors the most-used fields of fiber/middleware/cors with kit-friendly defaults.

type CacheDefaults

type CacheDefaults[T any] struct {
	// Storage backs the cache. Any fiber.Storage implementation works:
	// the gofiber/storage repo ships drivers for Redis, memcached,
	// PostgreSQL, S3, …
	//
	// Nil → Fiber's default in-process map. Convenient for a single
	// instance; in production use a shared backend so replicas share
	// one cache and restarts don't wipe it.
	Storage fiber.Storage

	// KeyBy returns a per-request fragment mixed into every cache key
	// for routes that opt into caching. Use it to scope cache entries
	// by tenant / user / role — anything that should invalidate
	// independently.
	//
	// SECURITY: when nil, the cache key is method + URL + vary
	// headers. If you cache a handler whose response depends on the
	// authenticated user, you MUST set KeyBy or one user's response
	// will be served to another.
	KeyBy func(c *Context[T]) string

	// MaxBytes caps Fiber's default in-process store. Ignored when
	// Storage is set. 0 means unlimited — do not use in production.
	MaxBytes uint
}

CacheDefaults holds the engine-wide knobs for the built-in response cache. Per-route TTL and flags live in the YAML `cache:` field; these defaults supply the bits that can't be expressed as strings.

type CacheInfo

type CacheInfo struct {
	TTL        string   `json:"ttl"`
	Control    bool     `json:"control,omitempty"`
	Headers    bool     `json:"headers,omitempty"`
	VaryHeader []string `json:"vary_header,omitempty"`
}

CacheInfo is the public introspection form of a route's cache configuration. nil → no cache.

type Checker

type Checker interface {
	// Name labels the check in the JSON response and operator logs.
	// Convention: short lowercase identifier (e.g. "db", "nats",
	// "redis", "upstream").
	Name() string
	// Check returns nil when the dependency is healthy. Any
	// non-nil error is rendered in the JSON body so K8s log
	// scrapers + operators can see WHY the probe failed.
	Check(ctx context.Context) error
}

Checker is the contract for one readiness probe. Each kit subsystem ships an adapter (`db.NewChecker`, `redisclient.NewChecker`, `natsclient.NewChecker`) so the typical Service wires the full dependency set without bespoke code.

Implementations should be cheap (single ping) and respect the supplied context's deadline — the Readiness handler runs them in parallel under one shared deadline.

type CompressionLevel

type CompressionLevel int

CompressionLevel mirrors the gzip/deflate level constants.

const (
	// CompressionDefault uses the implementation's default.
	CompressionDefault CompressionLevel = 0
	// CompressionBestSpeed minimises CPU overhead.
	CompressionBestSpeed CompressionLevel = 1
	// CompressionBestCompression maximises ratio at higher CPU cost.
	CompressionBestCompression CompressionLevel = 2
)

type Context

type Context[T any] struct {
	*fiber.Ctx
	Data T
}

Context is the per-request value passed to fibermap handlers and middleware. It embeds *fiber.Ctx so every Fiber method is available directly, and adds Data of type T — populated once per request by the ContextBuilder set on the engine.

func ContextFrom

func ContextFrom[T any](c *fiber.Ctx) (*Context[T], bool)

ContextFrom retrieves the per-request *Context[T] that fibermap's root middleware stashed on the given *fiber.Ctx. Returns (nil, false) when called on a request that didn't pass through a fibermap-mounted router, or when T does not match the engine's payload type.

Use this in code that operates on a plain fiber.Handler but still needs typed access to Context[T].Data — most commonly inside cache key generators or other adapters that bridge to non-fibermap middleware.

type ContextBuilder

type ContextBuilder[T any] func(c *fiber.Ctx) (T, error)

type ContextErrorFunc

type ContextErrorFunc func(c *fiber.Ctx, err error) error

type Engine

type Engine[T any] struct {
	// contains filtered or unexported fields
}

func Default

func Default[T any]() *Engine[T]

Default returns an Engine[T] pre-wired to also enable Prometheus WithMetrics when Engine.Run is called. Since v0.5, the other pieces of the ops bundle — Recover, RequestID, RequestLogger, and HealthCheck — are built into Run itself and on by default, so `New[T]().Run()` already gets them. The only thing Default adds is the metrics endpoint, which is opt-in because it pulls in `github.com/prometheus/client_golang`.

Use Default when you want the metrics endpoint without spelling it out:

eng := fibermap.Default[AppCtx]()
eng.SetContextBuilder(...)
eng.RegisterHandler(...)
eng.Run(fibermap.WithUse(auth.Bearer()))   // full bundle + auth

Use New when you don't want metrics. You can still get the rest of the ops bundle:

eng := fibermap.New[AppCtx]()              // recover + request_id + logger + healthz still on
eng.Run(fibermap.WithoutRecover())         // opt out of any default

To override a built-in default — change the health-check path, supply a custom slog.Logger, etc. — call the matching With* option:

eng.Run(fibermap.WithHealthCheck("/_health"))
eng.Run(fibermap.WithRecover(myLogger))

func New

func New[T any]() *Engine[T]

New constructs an empty Engine. Call SetContextBuilder, RegisterHandler, RegisterMiddleware (and optionally RegisterMiddlewareFactory) before LoadFile/LoadBytes and Mount.

func (*Engine[T]) Add

func (e *Engine[T]) Add(method, path, name string, h HandlerFunc[T], opts ...AddOpts)

Add registers a route programmatically — outside the YAML route tree. Useful for routes that don't fit the declarative model: debug/pprof endpoints, dynamic admin handlers, embedded UIs, etc.

The handler goes through the same per-request Context[T] wrapper as YAML routes; the engine's root middleware (contextInit) still populates `Context[T].Data` for the request.

Programmatic routes do NOT support middleware/timeout/cache via this API — those features are intentionally YAML-only to keep the declarative surface authoritative. If you need a programmatic route with middleware, install it directly on the fiber.App via WithConfigureApp.

Routes added via Add are surfaced on Engine.Routes() with Source = SourceProgrammatic, so introspection and `fibermaptest` see them.

Panics with *Error / CodeRegisterAfterMount if called after Mount. Panics with CodeInvalidHTTPMethod for unsupported methods. Empty name, empty path, or nil handler panic as a programmer error.

func (*Engine[T]) All

func (e *Engine[T]) All() iter.Seq[RouteInfo]

All returns a range-over-func iterator over every route registered during Mount, in Mount order. Each yielded RouteInfo is a defensive copy — safe to mutate without affecting engine state. Stop the iteration by returning from your loop body normally (break/return):

for r := range eng.All() {
    if strings.HasPrefix(r.Path, "/internal/") {
        continue
    }
    fmt.Println(r.Method, r.Path)
}

Prefer All over Walk when targeting Go 1.23+ — it is idiomatic (no ErrStopWalk sentinel needed) and supports break / continue natively. Walk stays for callers that need an error-propagating callback shape.

func (*Engine[T]) HandlerMeta

func (e *Engine[T]) HandlerMeta(name string) *HandlerMeta

HandlerMeta returns the schema metadata attached to a handler via HandlerOption values passed to Engine.RegisterHandler. Returns nil when no options were attached. The returned pointer aliases engine state — callers must treat it as read-only.

Primarily for introspection consumers like fibermap/openapi.

func (*Engine[T]) LoadBytes

func (e *Engine[T]) LoadBytes(data []byte) error

LoadBytes parses YAML from memory.

func (*Engine[T]) LoadFS

func (e *Engine[T]) LoadFS(fsys fs.FS, path string) error

LoadFS reads and parses a YAML file from an fs.FS — typically an embed.FS so the route definitions ship inside the binary.

//go:embed routes.yaml
var routesFS embed.FS
eng.LoadFS(routesFS, "routes.yaml")

func (*Engine[T]) LoadFile

func (e *Engine[T]) LoadFile(path string) error

LoadFile reads and parses a YAML file.

func (*Engine[T]) Lookup

func (e *Engine[T]) Lookup(method, path string) (RouteInfo, bool)

Lookup returns the RouteInfo for a given (method, path) pair, or (zero, false) if no such route was registered. method is matched exactly (case-sensitive); path matches the resolved path (including any inherited prefix) exactly. The returned RouteInfo is a defensive copy.

O(1) — the (method, path) → index map is built at Mount and consulted for every Lookup call. Walk and Routes still iterate the underlying slice in insertion order.

func (*Engine[T]) Mount

func (e *Engine[T]) Mount(router fiber.Router) error

Mount validates the loaded YAML against registered handlers/middleware and installs routes on `router`. If validation produces any errors they are all returned (errors.Join) and no routes are installed.

Calling Mount twice on the same engine returns an *Error with CodeAlreadyMounted.

func (*Engine[T]) RegisterHandler

func (e *Engine[T]) RegisterHandler(name string, h HandlerFunc[T], opts ...HandlerOption)

RegisterHandler registers a handler under a name referenced from YAML. Panics with *Error / CodeDuplicateRegistration if the name is already taken, or CodeRegisterAfterMount if called after Mount.

Optional HandlerOption values attach typed request / response schemas (read by fibermap/openapi for spec generation, ignored at runtime):

eng.RegisterHandler("tasks.create", h.Create,
    fibermap.WithBody(CreateReq{}),
    fibermap.WithResponse(201, Task{}),
    fibermap.WithResponse(400, ErrorResponse{}),
)

func (*Engine[T]) RegisterMiddleware

func (e *Engine[T]) RegisterMiddleware(name string, m MiddlewareFunc[T])

RegisterMiddleware registers a plain (no-args) middleware. YAML references it as a scalar string. Panics with *Error / CodeDuplicateRegistration if the name is already taken in either the plain or factory registry, or CodeRegisterAfterMount if called after Mount.

func (*Engine[T]) RegisterMiddlewareFactory

func (e *Engine[T]) RegisterMiddlewareFactory(name string, f MiddlewareFactoryFunc[T])

RegisterMiddlewareFactory registers a parameterized middleware. YAML references it as a single-key mapping {name: [args...]}. The factory is invoked once per (name, args) pair at Mount time; the returned MiddlewareFunc is cached for the lifetime of the engine. Panics with *Error / CodeDuplicateRegistration on name conflict, or CodeRegisterAfterMount if called after Mount.

Example

ExampleEngine_RegisterMiddlewareFactory shows the parameterized middleware mechanism — register a factory that returns a closure over its YAML-supplied args, then reference it from YAML as a single-key map.

package main

import (
	"fmt"

	"github.com/gofiber/fiber/v2"
	"github.com/theizzatbek/gokit/fibermap"
)

// docCtx is the per-request payload used by the examples below.
type docCtx struct {
	UserID string
	Role   string
}

// Alias example shows the recommended way to hide the generic parameter
// from handler signatures in user code.
type (
	docCtxRef = fibermap.Context[docCtx]
	docMW     = fibermap.MiddlewareFunc[docCtx]
)

func main() {
	eng := fibermap.New[docCtx]()
	eng.SetContextBuilder(func(c *fiber.Ctx) (docCtx, error) {
		return docCtx{Role: "admin"}, nil
	})

	eng.RegisterMiddlewareFactory("require_role", func(args []string) (docMW, error) {
		if len(args) == 0 {
			return nil, fmt.Errorf("require_role: at least one role required")
		}
		allowed := append([]string(nil), args...)
		return func(c *docCtxRef) error {
			for _, r := range allowed {
				if r == c.Data.Role {
					return c.Next()
				}
			}
			return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden"})
		}, nil
	})
	eng.RegisterHandler("create", func(c *docCtxRef) error { return c.SendString("ok") })

	if err := eng.LoadBytes([]byte(`
groups:
  - routes:
      - method: POST
        path: /things
        handler: create
        middleware:
          - require_role: [admin, director]
`)); err != nil {
		panic(err)
	}

	if err := eng.Validate(); err != nil {
		fmt.Println("invalid:", err)
		return
	}
	fmt.Println("config OK")
}
Output:
config OK

func (*Engine[T]) Routes

func (e *Engine[T]) Routes() []RouteInfo

Routes returns a snapshot of all routes registered during Mount. Returns an empty slice if called before Mount. The returned slice and each RouteInfo's slice fields are independent copies — mutating them will not affect engine state.

Example

ExampleEngine_Routes demonstrates introspection — after Mount, every installed route is available via Routes() with its resolved middleware chain (including factory args).

package main

import (
	"fmt"

	"github.com/gofiber/fiber/v2"
	"github.com/theizzatbek/gokit/fibermap"
)

// docCtx is the per-request payload used by the examples below.
type docCtx struct {
	UserID string
	Role   string
}

// Alias example shows the recommended way to hide the generic parameter
// from handler signatures in user code.
type docCtxRef = fibermap.Context[docCtx]

func main() {
	eng := fibermap.New[docCtx]()
	eng.SetContextBuilder(func(c *fiber.Ctx) (docCtx, error) { return docCtx{}, nil })
	eng.RegisterMiddleware("auth", func(c *docCtxRef) error { return c.Next() })
	eng.RegisterHandler("ping", func(c *docCtxRef) error { return c.SendString("pong") })

	if err := eng.LoadBytes([]byte(`
groups:
  - prefix: /v1
    middleware: [auth]
    routes:
      - { method: GET, path: /ping, handler: ping }
`)); err != nil {
		panic(err)
	}
	if err := eng.Mount(fiber.New()); err != nil {
		panic(err)
	}

	for _, r := range eng.Routes() {
		fmt.Printf("%s %s -> %s (mw=%d)\n", r.Method, r.Path, r.Handler, len(r.Middleware))
	}
}
Output:
GET /v1/ping -> ping (mw=1)

func (*Engine[T]) Run

func (e *Engine[T]) Run(opts ...RunOption) error

Run is the one-shot launcher. It creates (or uses) a fiber.App, installs Fiber-level middlewares, loads the YAML route tree (default "routes.yaml" on disk), mounts the engine, and blocks on Listen. On SIGINT/SIGTERM it triggers a graceful shutdown.

Returns nil on graceful shutdown, the first non-nil error from load / mount / listen otherwise.

The engine must have a ContextBuilder set and all referenced handlers / middleware / factories registered before Run — Run just calls Mount, which validates them.

Defaults — see RunOption documentation.

func (*Engine[T]) SetBindErrorHandler

func (e *Engine[T]) SetBindErrorHandler(fn BindErrorFunc[T])

SetBindErrorHandler customises how parse / validate failures inside [RegisterBody] / [RegisterQuery] / [RegisterParams] / [RegisterHeaders] are turned into responses. Default returns 400 with `{"error": err.Error()}`.

Inspect the error with errors.Is against bind.ErrParseBody / bind.ErrValidateBody (and the Query/Params/Header variants) to branch on parse vs validate.

func (*Engine[T]) SetCacheDefaults

func (e *Engine[T]) SetCacheDefaults(d CacheDefaults[T])

SetCacheDefaults installs engine-wide defaults for routes that declare `cache:` in YAML. Call once before Mount; later calls silently overwrite. See CacheDefaults for the security note about the KeyBy field.

func (*Engine[T]) SetContextBuilder

func (e *Engine[T]) SetContextBuilder(fn ContextBuilder[T])

SetContextBuilder installs the function that builds the per-request Data. Calling more than once silently overwrites.

func (*Engine[T]) SetContextErrorHandler

func (e *Engine[T]) SetContextErrorHandler(h ContextErrorFunc)

SetContextErrorHandler overrides the default response when ContextBuilder returns an error.

func (*Engine[T]) SetValidator

func (e *Engine[T]) SetValidator(v bind.Validator)

SetValidator installs the validator used by [RegisterBody], [RegisterQuery], [RegisterParams], and [RegisterHeaders]. nil disables validation (the parse step still runs; the validate step is skipped — same behaviour as bind.Body with a nil validator).

eng.SetValidator(validator.New(validator.WithRequiredStructEnabled()))

func (*Engine[T]) Validate

func (e *Engine[T]) Validate() error

Validate runs the same checks as Mount without installing any route on a Fiber router. Use it from CI scripts or tests to verify that a routes.yaml is consistent with the registered handlers, middleware, and factories. Returns the joined *Error values (errors.Join) or nil on success. Safe to call multiple times and at any point after a successful Load*.

func (*Engine[T]) Validator added in v1.1.0

func (e *Engine[T]) Validator() bind.Validator

Validator returns the bind.Validator currently installed via Engine.SetValidator. Useful for service-level introspection (e.g. type-asserting to *validator.Validate to register extra tags after the kit wires the default). Returns nil when no validator was set.

func (*Engine[T]) Walk

func (e *Engine[T]) Walk(fn func(r RouteInfo) error) error

Walk invokes fn for every route registered during Mount, in Mount order. Returning ErrStopWalk from fn ends the walk without propagating an error; any other non-nil error from fn is returned to the caller. Each RouteInfo passed to fn is a defensive copy — safe to mutate.

Walk is the building block for introspection consumers (OpenAPI generators, route-table CLIs, test helpers); use it instead of iterating Routes() when you might want to early-stop or filter. On Go 1.23+, prefer All() for idiomatic range-over-func.

type Error

type Error struct {
	Stage   string `json:"stage"`
	Code    string `json:"code"`
	Message string `json:"message"`
	File    string `json:"file,omitempty"`
	Line    int    `json:"line,omitempty"`
	Path    string `json:"path,omitempty"`
}

Error is the typed error returned by all fibermap operations. Stage is one of "parse", "mount", or "register". Code is one of the Code* constants. JSON tags allow structured logging or admin-endpoint exposure.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

type HandlerFunc

type HandlerFunc[T any] func(c *Context[T]) error

type HandlerMeta

type HandlerMeta struct {
	// Body is the request body model — typically a struct with
	// `json:` and `validate:` tags. nil when no body was declared.
	Body any
	// Query is the query-string model — fields use the `query:` tag.
	Query any
	// Params is the route-parameter model — fields use the `params:`
	// tag. When set, OpenAPI generation uses this struct's schema for
	// path parameters instead of synthesizing plain string entries
	// from the URL pattern (picks up validate-derived constraints,
	// descriptions, etc).
	Params any
	// Headers is the request-header model — fields use the
	// `reqHeader:` tag.
	Headers any
	// Responses maps HTTP status code to the response body model.
	// nil model on a status means "empty body" (use for 204).
	Responses map[int]any
}

HandlerMeta holds typed schemas attached to a handler via HandlerOption values passed to Engine.RegisterHandler. The fields are opaque `any` — pass the zero value of a Go struct (e.g. `CreateReq{}`); consumers reflect on the runtime type.

Returned by Engine.HandlerMeta. Callers must treat the returned pointer's contents as read-only — fibermap does not deep-copy.

type HandlerOption

type HandlerOption func(*HandlerMeta)

HandlerOption attaches typed request/response metadata to a handler at registration time. The metadata is stored as `any` — fibermap core does not interpret it. Consumers like fibermap/openapi reflect on the values to generate JSON-Schema documentation.

eng.RegisterHandler("tasks.create", taskH.Create,
    fibermap.WithBody(CreateReq{}),
    fibermap.WithResponse(201, Task{}),
    fibermap.WithResponse(400, ErrorResponse{}),
)

All options are optional — handlers registered without any are still valid; they just lack documented schemas.

func WithBody

func WithBody(model any) HandlerOption

WithBody attaches a request-body schema model to the handler. Pass the zero value of your request struct:

eng.RegisterHandler("tasks.create", h.Create, fibermap.WithBody(CreateReq{}))

func WithHeaders

func WithHeaders(model any) HandlerOption

WithHeaders attaches a request-header schema model.

func WithParams

func WithParams(model any) HandlerOption

WithParams attaches a route-parameter schema model — typically a struct with `params:` and `validate:` tags. OpenAPI generation reads the model's fields to enrich each path parameter with the declared schema (descriptions, validate-derived constraints, custom types). When omitted, path parameters fall back to plain `string` entries derived from the URL pattern.

func WithQuery

func WithQuery(model any) HandlerOption

WithQuery attaches a query-string schema model.

func WithResponse

func WithResponse(status int, model any) HandlerOption

WithResponse declares the schema for one HTTP response status. Multiple calls accumulate; passing nil for `model` advertises an empty body (typical for 204 No Content).

fibermap.WithResponse(201, Task{}),
fibermap.WithResponse(400, ErrorResponse{}),
fibermap.WithResponse(204, nil),

type IdempotencyLocker

type IdempotencyLocker interface {
	AcquireLock(ctx context.Context, key string, ttl time.Duration) (bool, error)
	ReleaseLock(ctx context.Context, key string) error
}

IdempotencyLocker is an OPTIONAL extension to IdempotencyStore that lets the middleware hold a short-lived lock around the in-flight handler. When a store implements this interface (e.g. the Redis-backed cache.RedisIdempotencyStore via SETNX), the middleware:

  1. Attempts AcquireLock(ctx, key, lockTTL) on cache miss.
  2. If acquired == false, returns 409 with Code CodeIdempotencyInFlight (another request with the same key is mid-flight). This closes the concurrent-replay race the plain Get/Set contract does not address.
  3. Runs the handler.
  4. Calls ReleaseLock(ctx, key) on exit (deferred — even on handler error).

The lock has a TTL so a crashing handler does not pin the key forever — failed locks roll off naturally. Lock TTL is tuned via WithIdempotencyLockTTL (default 30s).

Stores that do NOT implement IdempotencyLocker keep the pre-locking behaviour (two concurrent requests may both run the handler — middleware assumes downstream idempotency).

Opt out at the middleware via WithIdempotencyWithoutLock even when the store implements the locker.

type IdempotencyOption

type IdempotencyOption func(*idempotencyConfig)

IdempotencyOption tunes IdempotencyKey.

func WithIdempotencyHeader

func WithIdempotencyHeader(name string) IdempotencyOption

WithIdempotencyHeader overrides the header name. Default is "X-Idempotency-Key" (the convention Stripe / GitHub use). Lowercase variants are not auto-handled — Fiber's Get is case-insensitive so callers don't need to worry about it.

func WithIdempotencyLockTTL

func WithIdempotencyLockTTL(d time.Duration) IdempotencyOption

WithIdempotencyLockTTL sets the TTL on the SETNX-style concurrency lock the middleware places around in-flight handlers (when the store implements IdempotencyLocker). The lock auto-expires so a crashing handler does not pin the key indefinitely.

Default 30s. Tune up for slow downstream calls (payment captures, SMS sends), down for very fast handlers. Must be shorter than the idempotency TTL — the lock guards the IN-FLIGHT window, not the replay window.

func WithIdempotencyMaxBodySize

func WithIdempotencyMaxBodySize(n int) IdempotencyOption

WithIdempotencyMaxBodySize caps the response body the middleware will cache. Larger responses pass through uncached and a Warn-level log records the skipped key. Default 1 MiB.

The cap exists because the store typically lives in Redis or another shared cache and an unbounded cap is a memory-pressure foot-gun.

func WithIdempotencyMethods

func WithIdempotencyMethods(methods ...string) IdempotencyOption

WithIdempotencyMethods restricts the methods the middleware caches. Default: POST, PUT, PATCH, DELETE — the methods that mutate state. Pass to add GET (read-through cache flavour) or to narrow to just POST.

func WithIdempotencyRequired

func WithIdempotencyRequired() IdempotencyOption

WithIdempotencyRequired switches the middleware into "header required" mode: requests without the header return 400 with Code CodeIdempotencyKeyMissing instead of passing through unaltered.

Use on critical write endpoints (payment capture, refund, transfer) where the kit-level guarantee is part of the contract. Leave off on routes where the client may legitimately not care about idempotency.

func WithIdempotencySkipStatus

func WithIdempotencySkipStatus(statuses ...int) IdempotencyOption

WithIdempotencySkipStatus marks status codes that should NOT be cached. The middleware passes them through uncached so e.g. a 500 from a transient downstream doesn't get pinned for hours.

Default: 5xx are skipped (the response is not durable enough to replay). Calling this REPLACES the default set; pass 500, 502, 503, 504 etc. explicitly if you want both default + extras.

func WithIdempotencyTTL

func WithIdempotencyTTL(d time.Duration) IdempotencyOption

WithIdempotencyTTL sets how long a captured response is replayable for. Default 24h. Tune down for high-volume endpoints (memory), up for slow-converging downstream systems (payment confirmations).

func WithIdempotencyWithoutLock

func WithIdempotencyWithoutLock() IdempotencyOption

WithIdempotencyWithoutLock disables the IdempotencyLocker path even when the store implements it. Pre-lock behaviour: two concurrent requests with the same key may BOTH run the handler — assumes downstream idempotency at the DB / queue layer.

Use sparingly. The locker default is the safer choice; opt out only on routes where the in-flight-409 response is unacceptable (e.g. very-long-running handlers where 409 noise drowns out real conflicts).

type IdempotencyStore

type IdempotencyStore interface {
	Get(ctx context.Context, key string) (*StoredResponse, error)
	Set(ctx context.Context, key string, resp *StoredResponse, ttl time.Duration) error
}

IdempotencyStore is the persistence backend IdempotencyKey uses to keep the captured response between the first and replay requests. Get returns nil on miss (NOT an error); Set replaces any prior entry. Errors are caller-defined — the middleware logs and continues on Get failure, logs on Set failure (the foreground request still completes).

clients/cache supplies a default implementation backed by Redis[StoredResponse]. Roll your own for non-Redis backends (BadgerDB, ristretto, in-memory map for tests).

type MetricsRegistry

type MetricsRegistry interface {
	prometheus.Registerer
	prometheus.Gatherer
}

MetricsRegistry is the registry shape WithMetricsRegistry accepts — both prometheus.Registerer (so fibermap can register its three HTTP collectors) and prometheus.Gatherer (so the scrape handler can serialise the current values). *prometheus.Registry satisfies both.

type MiddlewareFactoryFunc

type MiddlewareFactoryFunc[T any] func(args []string) (MiddlewareFunc[T], error)

type MiddlewareFunc

type MiddlewareFunc[T any] func(c *Context[T]) error

type MiddlewareRef

type MiddlewareRef struct {
	Name string   `json:"name"`
	Args []string `json:"args,omitempty"`
}

MiddlewareRef is the public form of mwRef surfaced via RouteInfo. Args is nil for plain (scalar) middleware, non-nil for factory calls (even if the factory was invoked with zero args).

type RateLimitConfig

type RateLimitConfig struct {
	// Max requests per Expiration window. Required (> 0).
	Max int

	// Expiration window. Default 1 minute.
	Expiration time.Duration

	// SkipPaths are exact paths to bypass rate limiting (healthz,
	// metrics, readyz). Empty = limit every path.
	SkipPaths []string

	// KeyGenerator returns the bucket key for a request. Default =
	// c.IP(). Override for per-user / per-API-key limits.
	KeyGenerator func(c *fiber.Ctx) string
}

RateLimitConfig configures RateLimit.

type ReadinessOption

type ReadinessOption func(*readinessConfig)

ReadinessOption configures Readiness.

func WithReadinessTimeout

func WithReadinessTimeout(d time.Duration) ReadinessOption

WithReadinessTimeout caps how long Readiness waits on the slowest checker. Default 5s. Lower in environments where K8s readiness probes are tight; raise when a check is legitimately slow (cross-region SaaS ping, etc.).

type RequestLoggerOption

type RequestLoggerOption func(*reqLoggerConfig)

RequestLoggerOption configures RequestLoggerWithOptions. The back-compat RequestLogger constructor wraps these internally.

func WithReqLogSkipPaths

func WithReqLogSkipPaths(paths ...string) RequestLoggerOption

WithReqLogSkipPaths sets the skip-path allowlist. Equivalent to passing skipPaths to RequestLogger directly.

func WithReqLogSlowThreshold

func WithReqLogSlowThreshold(d time.Duration) RequestLoggerOption

WithReqLogSlowThreshold demotes fast requests (latency < d) to Debug level and promotes slow ones (>= d) to Warn level. 5xx always stays at Error regardless of latency. Default 0 = no threshold (every non-5xx request logged at Info — current behaviour).

Use to keep noisy per-second polling logs out of production dashboards while still surfacing genuinely slow paths.

type RouteInfo

type RouteInfo struct {
	Method      string          `json:"method"`
	Path        string          `json:"path"`
	Handler     string          `json:"handler"`
	Name        string          `json:"name,omitempty"`
	Summary     string          `json:"summary,omitempty"`
	Description string          `json:"description,omitempty"`
	Middleware  []MiddlewareRef `json:"middleware,omitempty"`
	Tags        []string        `json:"tags,omitempty"`
	Timeout     string          `json:"timeout,omitempty"`
	Cache       *CacheInfo      `json:"cache,omitempty"`
	Source      string          `json:"source,omitempty"`
}

RouteInfo is the public introspection record returned by Engine.Routes(). JSON tags are provided so users can expose Routes() over an admin endpoint or dump it for tooling without an extra wrapper struct.

Timeout is the verbatim YAML duration string ("5s") — empty when no per-route timeout was declared. Kept as a string so JSON admin-endpoint output stays human-readable.

Source identifies where the route came from: "yaml" for routes declared in routes.yaml, "programmatic" for routes added via Engine.Add. Useful for ops tools that want to distinguish declarative from imperative routes.

type RunOption

type RunOption func(*runConfig)

RunOption configures Engine.Run. Default behaviour with zero options:

  • Create a fresh fiber.App via fiber.New().
  • Load routes from "routes.yaml" on disk (skipped if the engine already loaded a YAML document).
  • Mount on the new app.
  • Listen on $PORT if set ("PORT=8080" → ":8080"), else ":3000".
  • On SIGINT/SIGTERM, gracefully drain in-flight requests for up to 10s, then exit.

func WithAddr

func WithAddr(addr string) RunOption

WithAddr overrides the listen address. When unset, Run picks up the `PORT` environment variable (cloud-platform convention: Heroku, Cloud Run, fly.io, Railway) and listens on `:${PORT}`; if `PORT` is also unset, defaults to ":3000". WithAddr always wins over `PORT`.

func WithBodyLimit

func WithBodyLimit(maxBytes int) RunOption

WithBodyLimit installs a Fiber-level body-size cap. Requests with Content-Length > maxBytes (or a body that exceeds it mid-read) surface as 413 Request Entity Too Large.

Sets fiber.Config.BodyLimit so the cap fires inside Fiber's parser BEFORE the body reaches the handler — defence-in-depth against huge-upload attacks.

func WithCORS

func WithCORS(cfg ...CORSConfig) RunOption

WithCORS installs the CORS middleware on the App level (BEFORE WithUse handlers). Pass a zero CORSConfig for kit defaults (allow any origin, common methods/headers).

func WithCompression

func WithCompression(level ...CompressionLevel) RunOption

WithCompression installs gzip/deflate response compression based on the request's Accept-Encoding header. Default level is CompressionBestSpeed — minimises CPU at modest size cost.

func WithConfigureApp

func WithConfigureApp(fn func(*fiber.App)) RunOption

WithConfigureApp is the escape hatch: a callback that gets the freshly-created *fiber.App after WithUse has been applied but before the engine mounts. Use it for anything WithUse can't express (groups, route-level handlers outside fibermap, ETag middleware, …).

func WithFiberConfig

func WithFiberConfig(cfg fiber.Config) RunOption

WithFiberConfig customizes fiber.New's argument. If not set, fiber.New() is called with no config.

func WithHealthCheck

func WithHealthCheck(path string) RunOption

WithHealthCheck registers a `GET` handler at `path` returning 200 OK with body "ok". The route is installed BEFORE any other middleware (WithRecover, WithUse, ContextBuilder) so it is not subject to auth, context construction, or panic-bound code paths — exactly what you want for a k8s livenessProbe / readinessProbe.

The endpoint does NOT appear in Engine.Routes() because it lives outside the engine's planned route set.

HealthCheck is ON BY DEFAULT in Run at path `/healthz` — call WithHealthCheck only to move it (e.g. `WithHealthCheck("/_health")`), pass empty string to disable, or use WithoutHealthCheck.

func WithMetrics

func WithMetrics(path string) RunOption

WithMetrics installs the Prometheus metrics middleware from Metrics and exposes the metrics at `path` (Prometheus text format). Default path "/metrics"; pass empty string to disable explicitly.

The middleware is registered AFTER WithRecover / WithRequestLogger so panics and request logs still happen as usual, but counts of served requests reflect what actually succeeded.

Metrics is OFF BY DEFAULT in Run (it pulls `github.com/prometheus/client_golang` into the binary). Use Default or call WithMetrics explicitly to enable it.

eng.Run(fibermap.WithMetrics("/metrics"))

func WithMetricsRegistry

func WithMetricsRegistry(reg MetricsRegistry) RunOption

WithMetricsRegistry routes the fibermap HTTP middleware metrics AND the /metrics scrape endpoint through the caller-provided registry instead of a private one created by Metrics. Use this to unify the kit's per-subsystem collectors (db, httpc, nats, …) so a single scrape returns the full picture:

reg := prometheus.NewRegistry()
db.WithMetrics(reg) // applied during db.Connect
httpc.WithMetrics(reg)
eng.Run(fibermap.WithMetricsRegistry(reg))

Pairs with the [service] subpackage, which auto-applies its own registry to every subsystem and passes it to Run via this option.

Has no effect on its own — WithMetrics (or Default) must also be in play to install the middleware + endpoint at all. Implicitly enables WithMetrics with path "/metrics" if neither WithMetrics nor WithoutMetrics was set.

func WithNotFoundHandler

func WithNotFoundHandler(h fiber.Handler) RunOption

WithNotFoundHandler installs a catch-all handler for unmatched routes (404). Default = Fiber's plain `404 Not Found`. The kit ships NotFoundJSON as a JSON-shape default.

eng.Run(fibermap.WithNotFoundHandler(fibermap.NotFoundJSON()))

func WithRateLimit

func WithRateLimit(rps float64, burst int, skipPaths ...string) RunOption

WithRateLimit installs an in-process IP-keyed token-bucket rate limiter at the App level. rps is sustained requests per second per IP; burst is the bucket size. skipPaths defaults to `/healthz, /readyz, /metrics` so k8s probes always pass.

In-process limit — for multi-replica deployments use the fiber/middleware/limiter Storage pattern with Redis backing via WithUse.

func WithReadiness

func WithReadiness(path string, checkers ...Checker) RunOption

WithReadiness installs a readiness probe at `path` that runs the supplied Checker set in parallel and returns 200 with `{"status":"ok"}` if all pass OR 503 with `{"status":"degraded","checks":{...}}` if any fail. Like /healthz the route is wired BEFORE any middleware so it isn't blocked by auth, recover, or any user-installed Use chain — K8s readiness probes always reach the kit's check logic.

Off by default at the fibermap layer; service.New auto-installs it from the wired subsystem set (DB, NATS, Redis) at `/readyz`. Customise the per-probe timeout via WithReadinessTimeout.

eng.Run(fibermap.WithReadiness("/readyz", svc.ReadinessCheckers()...))

func WithReadinessOpts

func WithReadinessOpts(opts ...ReadinessOption) RunOption

WithReadinessOpts forwards ReadinessOption values to the readiness handler installed by WithReadiness. No-op when WithReadiness was not also passed.

func WithRecover

func WithRecover(logger *slog.Logger) RunOption

WithRecover installs Recover as the FIRST Fiber-level middleware (before WithUse handlers). Panics in any downstream middleware or handler are logged with the request's method, path, request_id, and a full stack trace via the given logger, and the client gets a generic 500 instead of a dropped connection. Pass nil for slog.Default().

Recover is ON BY DEFAULT in Run — call WithRecover only to supply a custom logger, or WithoutRecover to disable.

func WithReqLogSlowThresholdOption

func WithReqLogSlowThresholdOption(d time.Duration) RunOption

WithReqLogSlowThresholdOption raises the RequestLogger's level based on per-request latency:

  • latency >= threshold → Warn
  • latency < threshold → Debug
  • status >= 500 → Error (always)

Default 0 = no threshold (every non-5xx request logged at Info — legacy behaviour).

Plays nicely with WithRequestLogger — pass both: WithRequestLogger supplies the logger + skipPaths, this option adds the level split.

func WithRequestLogger

func WithRequestLogger(logger *slog.Logger, skipPaths ...string) RunOption

WithRequestLogger installs RequestLogger in the Use chain AFTER WithRecover (so panics still get recovered and logged separately). Skip paths are typically `/healthz` and `/metrics` — pass them via `skipPaths`. Empty (or nil) logger falls back to slog.Default.

RequestLogger is ON BY DEFAULT in Run (with `slog.Default()` and skipPaths `[/healthz, /metrics]`) — call this only to supply a custom logger or skip-set, or WithoutRequestLogger to disable.

func WithRoutesFS

func WithRoutesFS(fsys fs.FS) RunOption

WithRoutesFS makes Run resolve RoutesPath via this fs.FS — typically an embed.FS so the YAML ships inside the binary:

//go:embed routes.yaml
var routesFS embed.FS

eng.Run(fibermap.WithRoutesFS(routesFS))

Ignored if the engine already loaded a YAML document.

func WithRoutesPath

func WithRoutesPath(path string) RunOption

WithRoutesPath overrides the YAML filename Run loads. Default "routes.yaml". Ignored if the engine already loaded a YAML document via LoadFile / LoadBytes / LoadFS before Run.

func WithShutdownTimeout

func WithShutdownTimeout(d time.Duration) RunOption

WithShutdownTimeout overrides how long graceful shutdown waits for in-flight requests. Default 10s. Pass 0 or negative to disable graceful shutdown — Run then exits immediately on signal (or only when Listen returns).

func WithTrustedProxies

func WithTrustedProxies(cidrs ...string) RunOption

WithTrustedProxies enables Fiber's proxy-header trust path — `c.IP()` returns the rightmost address from X-Forwarded-For (or configured ProxyHeader) only if the immediate hop's IP is in the supplied CIDR allowlist. Without this, c.IP() returns the TCP peer address (typically the load balancer's IP) — bad for IP-based rate limiting / audit logs.

Example:

fibermap.WithTrustedProxies("10.0.0.0/8", "192.168.0.0/16")

Pass at least the cluster's pod CIDR and any load balancer's egress range.

func WithUse

func WithUse(handlers ...fiber.Handler) RunOption

WithUse installs Fiber-level middlewares via app.Use BEFORE the engine mounts. Use this for auth / request-id / logging that must run before fibermap's ContextBuilder (so locals are populated when the builder reads them).

Can be passed multiple times; handlers are concatenated in order.

func WithoutHealthCheck

func WithoutHealthCheck() RunOption

WithoutHealthCheck disables the built-in health-check route that Run otherwise installs by default. Equivalent to `WithHealthCheck("")`.

func WithoutMetrics

func WithoutMetrics() RunOption

WithoutMetrics suppresses the Prometheus metrics endpoint. Useful in combination with Default when you want every default EXCEPT metrics.

func WithoutRecover

func WithoutRecover() RunOption

WithoutRecover disables the built-in Recover middleware that Run otherwise installs by default. Use only when you have your own panic-handling wired (e.g. via Fiber's ErrorHandler or WithConfigureApp).

func WithoutRequestID

func WithoutRequestID() RunOption

WithoutRequestID suppresses the built-in RequestID middleware that Run prepends to the Use chain by default. Use when you have a custom request-correlation scheme you're installing yourself via WithUse.

func WithoutRequestLogger

func WithoutRequestLogger() RunOption

WithoutRequestLogger disables the built-in RequestLogger that Run otherwise installs by default. Use when you log access yourself (e.g. via a different middleware in WithUse) and don't want a duplicate line.

func WithoutSignalHandling

func WithoutSignalHandling() RunOption

WithoutSignalHandling disables Run's SIGINT/SIGTERM trap entirely. Use when embedding Run in a parent that owns process signals.

type SecurityHeadersOption

type SecurityHeadersOption func(*securityHeadersConfig)

SecurityHeadersOption tunes SecurityHeaders response headers.

func WithCSP

func WithCSP(policy string) SecurityHeadersOption

WithCSP overrides the default Content-Security-Policy value. The default policy is API-friendly: `default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`

For services that render HTML, tighten further (script-src, style-src, img-src). For pure JSON APIs, the default is the safe minimum.

func WithFrameOptions

func WithFrameOptions(value string) SecurityHeadersOption

WithFrameOptions overrides X-Frame-Options. Default "DENY". Common alternative: "SAMEORIGIN" for services that legitimately embed themselves. CSP frame-ancestors is the modern equivalent and is set via WithCSP.

func WithHSTSIncludeSubdomains

func WithHSTSIncludeSubdomains() SecurityHeadersOption

WithHSTSIncludeSubdomains appends "includeSubDomains" to the HSTS header — every subdomain of this origin must also be HTTPS-only. Off by default because turning it on retroactively breaks any plain-HTTP subdomain.

func WithHSTSMaxAge

func WithHSTSMaxAge(seconds int) SecurityHeadersOption

WithHSTSMaxAge overrides the Strict-Transport-Security max-age directive (seconds). Default is 1 year. HSTS is on by default — disable via WithoutHSTS when the deployment isn't HTTPS-only.

func WithHSTSPreload

func WithHSTSPreload() SecurityHeadersOption

WithHSTSPreload appends "preload" to the HSTS header. Only set once the domain is registered at https://hstspreload.org and includeSubDomains is also enabled — browsers reject preload without it.

func WithReferrerPolicy

func WithReferrerPolicy(value string) SecurityHeadersOption

WithReferrerPolicy overrides Referrer-Policy. Default "strict-origin-when-cross-origin" — same-origin requests get the full URL, cross-origin gets only the origin, HTTP requests from HTTPS pages get nothing.

func WithoutCSP

func WithoutCSP() SecurityHeadersOption

WithoutCSP suppresses the Content-Security-Policy header. Other headers remain installed.

func WithoutHSTS

func WithoutHSTS() SecurityHeadersOption

WithoutHSTS suppresses the Strict-Transport-Security header. Use when the service is reachable over plain HTTP by design (internal-only, dev cluster). Other headers remain installed.

type StoredResponse

type StoredResponse struct {
	Status      int    `json:"status"`
	Body        []byte `json:"body"`
	ContentType string `json:"content_type"`
}

StoredResponse is the captured shape replayed on subsequent hits with the same idempotency key. Marshalable by encoding/json so any kit-shaped cache (clients/cache.Redis[StoredResponse]) works.

Only the body, status, and Content-Type are replayed — other response headers (Set-Cookie, X-Request-ID, custom domain headers) intentionally do NOT survive. Replaying Set-Cookie would hand a stale session to a different caller; X-Request-ID belongs to the new request not the original. Keep the stored shape minimal so the contract stays understandable.

Directories

Path Synopsis
Package bind provides typed request parse + validation helpers for fibermap (or any Fiber) handlers.
Package bind provides typed request parse + validation helpers for fibermap (or any Fiber) handlers.
Package dev wires the kit's dev-only DX tooling — HTML error pages with stack traces, route/config inspectors.
Package dev wires the kit's dev-only DX tooling — HTML error pages with stack traces, route/config inspectors.
Package factory ships ready-made middleware factories and adapters for fibermap.Engine.
Package factory ships ready-made middleware factories and adapters for fibermap.Engine.
Package fibermaptest provides assertion helpers for fibermap engines.
Package fibermaptest provides assertion helpers for fibermap engines.
Package openapi generates OpenAPI 3.0 specifications from a fibermap fibermap.Engine.
Package openapi generates OpenAPI 3.0 specifications from a fibermap fibermap.Engine.
Package sse adds Server-Sent Events handlers to fibermap.
Package sse adds Server-Sent Events handlers to fibermap.
Package uploadguard is a Fiber middleware that validates an inbound multipart-form file field BEFORE the handler sees it.
Package uploadguard is a Fiber middleware that validates an inbound multipart-form file field BEFORE the handler sees it.
Package webhookguard ships a fiber middleware that verifies the signature of an inbound webhook payload against a clients/webhooks.Verifier.
Package webhookguard ships a fiber middleware that verifies the signature of an inbound webhook payload against a clients/webhooks.Verifier.
Package ws adds WebSocket handlers to fibermap on top of github.com/gofiber/websocket/v2.
Package ws adds WebSocket handlers to fibermap on top of github.com/gofiber/websocket/v2.
Package wsnats bridges browser WebSocket clients to NATS pub/sub.
Package wsnats bridges browser WebSocket clients to NATS pub/sub.

Jump to

Keyboard shortcuts

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