fibr

module
v2.0.0 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2026 License: MIT

README ΒΆ

Fibr

ci codecov Go Reference Go Report Card Release

A collection of helper packages for the Fiber web framework.

πŸ“– Documentation Β· API reference (pkg.go.dev)

Install

go get github.com/rahmadafandi/fibr/v2

Requires Go 1.26+. Targets Fiber v3 and Bun ORM (Postgres or SQLite).

Stability

fibr follows Semantic Versioning. This is the v2 line (module path github.com/rahmadafandi/fibr/v2), which targets Fiber v3:

  • No breaking changes within v2.x. Exported identifiers will not be removed or changed incompatibly until a v3 major release.
  • New functionality arrives in backward-compatible minor releases; fixes in patch releases.
  • Coming from v1 (Fiber v2)? See MIGRATING.md. v1 stays available at github.com/rahmadafandi/fibr and is maintained on the v1 branch.
  • The generated-app scaffolding (fibr new) follows the same library version but generated code is a starting point you own β€” regenerating is never required.

Not covered by the guarantee: unexported APIs, behavior explicitly documented as experimental, and transitive dependency internals.

Quickstart

package main

import (
    "fmt"

    "github.com/gofiber/fiber/v3"
    "github.com/rahmadafandi/fibr/v2/bootstrap"
    "github.com/rahmadafandi/fibr/v2/config"
    "github.com/rahmadafandi/fibr/v2/database"
    "github.com/rahmadafandi/fibr/v2/health"
    "github.com/rahmadafandi/fibr/v2/response"
)

func main() {
    type Config struct {
        DatabaseURL string `mapstructure:"DATABASE_URL" default:"file::memory:?cache=shared"`
    }

    var cfg Config
    if err := config.LoadConfig(&cfg); err != nil {
        panic(err)
    }

    db, err := database.NewBun(cfg.DatabaseURL)
    if err != nil {
        panic(err)
    }

    app := bootstrap.New(bootstrap.Options{
        DB:           db,
        CORSOrigins:  []string{"https://app.example.com"},
        RateLimit:    100,
        HealthChecks: []health.NamedCheck{health.PingBun(db)},
    })

    app.Get("/", func(c fiber.Ctx) error {
        return response.SendSuccess(c, "Hello, World!", "Welcome")
    })

    fmt.Println("Server listening on :3000")
    if err := app.Run(":3000"); err != nil {
        panic(err)
    }
}

Architecture

For how the packages layer and how bootstrap composes them into an app, see ARCHITECTURE.md.

Package Index

  • config β€” Load env vars into typed structs with default and required tags.
  • logger β€” Structured logger based on zerolog.
  • response β€” Standardized JSON response helpers.
  • parser β€” Bun pagination/search query modifiers, including keyset (cursor) pagination.
  • pagination β€” Paginated result envelope with page metadata; offset (NewPagination) and cursor (CursorPage) variants.
  • validator β€” Struct validation with custom rules and JSON field names.
  • bind β€” Parse and validate a request body/query/params into T in one call; writes 400/422 on failure.
  • jwt β€” JWT generation and validation helpers.
  • httpclient β€” Context-aware JSON HTTP client with retry.
  • retry β€” Generic retry with exponential backoff + jitter (Do/DoValue).
  • rediskit β€” Redis wrapper with Remember cache-aside helper. Includes a Storage adapter (NewStorage) for Redis-backed rate limiting.
  • slug β€” Unique URL-safe slug generator backed by a Bun database.
  • uploader β€” Local file uploader with size and MIME limits. Also includes S3Uploader for S3-compatible storage (AWS S3, MinIO, R2).
  • middleware β€” Recover, request logging, and request-id middleware.
  • reqctx β€” Request context, request-id, and type-safe local accessors.
  • database β€” Bun connector with Postgres/SQLite dialect auto-detection.
  • dbresolver β€” Read/write split over Bun: Writer() (primary) and Reader() (round-robin replica).
  • migrate β€” Versioned migrations with bun/migrate and a ready cobra command.
  • auth β€” JWT bearer authentication and bcrypt password hashing for Fiber.
  • health β€” Liveness (/livez) and readiness (/readyz) endpoints.
  • metrics β€” Prometheus request metrics middleware + /metrics handler.
  • tracing β€” OpenTelemetry tracing setup (OTLP/HTTP) + Fiber spans.
  • jobs β€” Redis-backed background jobs (asynq) + asynqmon monitoring mount. Includes Scheduler for cron-triggered (periodic) jobs.
  • lock β€” Single-instance Redis distributed mutex (TryAcquire/Acquire/Do, owner-only Release/Extend) for single-execution across replicas.
  • outbox β€” Transactional outbox: enqueue events in the same DB transaction as your writes; a background relay publishes them at-least-once.
  • events β€” In-process typed event bus (Subscribe[T]/Publish[T]), sync by default with opt-in async.
  • inbox β€” Idempotent-consumer dedup (Once); consumer-side complement to outbox.
  • featureflag β€” Runtime flags (boolean, percentage rollout, per-user/group) via Static/Rules/Redis providers + Fiber helper.
  • audit β€” Structured audit log (actor/action/target) persisted via Bun, with a Fiber request helper.
  • cache β€” Generic in-memory cache (Cache[V]) with TTL, LRU eviction, and singleflight GetOrLoad.
  • ratelimit β€” Redis token-bucket rate limiter (per-key, cost-aware) + Fiber middleware.
  • apikey β€” API-key auth (hashed keys, scopes, pluggable store) + Fiber middleware.
  • mailer β€” Transactional email: pluggable Sender (SMTP/log/memory) + template render.
  • server β€” Signal-based graceful shutdown via RunGraceful.
  • apierror β€” Typed HTTP errors (BadRequest, NotFound, Conflict, ...) with a Fiber ErrorHandler; installed automatically by bootstrap.
  • bootstrap β€” One-call app wiring: middleware, health, DB, and graceful shutdown.
  • fibrtest β€” Test harness: fluent HTTP client over *fiber.App, response assertions, in-memory DB + JWT helpers.

Packages

config

Loads configuration from environment variables (and a .env file if present) into a typed struct. Supports default:"..." and required:"true" tags, as well as time.Duration, float, bool, integer, and comma-separated string-slice fields.

Usage:

import "github.com/rahmadafandi/fibr/v2/config"

type AppConfig struct {
    Port    int           `mapstructure:"port"     default:"8080"`
    DBURL   string        `mapstructure:"db_url"   required:"true"`
    Timeout time.Duration `mapstructure:"timeout"  default:"30s"`
    Hosts   []string      `mapstructure:"hosts"    default:"a,b,c"`
}

var cfg AppConfig
err := config.LoadConfig(&cfg)
logger

A structured logger based on zerolog.

Usage:

import "github.com/rahmadafandi/fibr/v2/logger"

log := logger.Default()
log.Info("Hello, world!")
response

Helper functions for sending standardized JSON responses.

Usage:

import "github.com/rahmadafandi/fibr/v2/response"

response.SendSuccess(c, data, "Success")
response.SendError(c, nil, "Error", 400)
parser

Helper functions for pagination with Bun. The Paginate helper returns a Bun query modifier for use with query.Apply.

Request body/query/params parsing uses Fiber's built-in c.Bind().Body(&out), c.Bind().Query(&out), and c.Bind().URI(&out) directly.

Usage:

import "github.com/rahmadafandi/fibr/v2/parser"

type MyStruct struct {
    Name string `json:"name"`
}

// Pagination with Bun
pq := &parser.PaginationQuery{}
if err := c.Bind().Query(pq); err != nil { ... }
if err := pq.Validate([]string{"name", "created_at"}); err != nil { ... }

var rows []MyModel
err = db.NewSelect().Model(&rows).Apply(parser.Paginate(pq, []string{"name"})).Scan(ctx)

// Count with search filter
count, err := db.NewSelect().Model(&rows).Apply(parser.Count(pq.Search, []string{"name"})).Count(ctx)

Search is case-insensitive on every supported database (ILIKE on Postgres, LIKE elsewhere). The page size is clamped to parser.MaxLimit (default 1000) so a client cannot request an unbounded page; lower it at startup (parser.MaxLimit = 100) or set it to 0 to disable the cap. The same cap applies to Keyset.

Keyset (cursor) pagination

For large tables or infinite scroll, parser.Keyset paginates by a cursor instead of an offset β€” it seeks straight to the cursor position on an index, so it stays O(limit) at any depth and is stable under concurrent inserts (no shifting rows). It supports forward and backward navigation via opaque cursors.

var kq parser.KeysetQuery // bound from ?limit=20&cursor=...&before=...
_ = c.Bind().Query(&kq)

// The columns must form a unique total ordering; make the last a tiebreaker (the pk).
cols := []parser.KeysetColumn{{Name: "created_at", Desc: true}, {Name: "id", Desc: true}}

var rows []Article
err := db.NewSelect().Model(&rows).Apply(parser.Keyset(kq, cols)).Scan(ctx)

page := pagination.NewCursorPage(rows, kq, cols, func(a Article) []any {
    return []any{a.CreatedAt, a.ID} // values in the same order as cols
})
// page.Data, page.NextCursor (""=last), page.PrevCursor (""=first)

Supported cursor value types: integers, strings, bools, and time.Time (encoded RFC3339). Keyset trades away total/page counts β€” keep offset NewPagination when you need a count.

pagination

Builds a paginated result envelope (data plus page metadata) for any element type. Guards against a zero page size and clamps page numbers below 1. For cursor-based paging, see CursorPage / NewCursorPage (above, under parser).

Usage:

import "github.com/rahmadafandi/fibr/v2/pagination"

p := pagination.NewPagination(rows, pq.Limit, pq.Page, totalCount)
// p.Data, p.PageSize, p.Count, p.TotalCount, p.PageCount, p.PageNumber, p.StartNumber
return response.SendSuccess(c, p, "ok")
validator

A helper package for validating structs using go-playground/validator. Supports custom validation rules via Register.

Usage:

import "github.com/rahmadafandi/fibr/v2/validator"

type MyStruct struct {
    Name string `json:"name" validate:"required"`
}

var body MyStruct

if errs := validator.ValidateStruct(&body); len(errs) > 0 {
    // Handle validation errors
}

// Register a custom rule (call once at startup, before concurrent use)
validator.Register("my_rule", func(fl validator.FieldLevel) bool {
    return fl.Field().String() != "forbidden"
})
Parse + validate with bind

bind.Body[T], bind.Query[T], and bind.Params[T] decode a request into T and run validator.ValidateStruct in one call. On malformed input they write a 400; on a validation failure a 422 with per-field errors; otherwise they return (value, true).

type CreateInput struct {
    Email string `json:"email" validate:"required,email"`
}

func create(c fiber.Ctx) error {
    in, ok := bind.Body[CreateInput](c)
    if !ok {
        return nil // bind wrote a 400 (malformed) or 422 (validation) response
    }
    return response.SendSuccess(c, in, "ok")
}
jwt

A helper package for working with JSON Web Tokens.

Usage:

import "github.com/rahmadafandi/fibr/v2/jwt"

// Generate a token that expires (preferred)
token, err := jwt.GenerateTokenWithExpiry(claims, secret, 15*time.Minute)

// Generate a token without an "exp" claim (valid until the secret changes)
token, err = jwt.GenerateToken(claims, secret)

// Validate a token
valid, err := jwt.ValidateToken(token, secret)

An empty secret is rejected with jwt.ErrEmptySecret by all three functions β€” a missing JWT_SECRET fails loudly instead of producing forgeable tokens.

httpclient

A small JSON HTTP client built on fasthttp. All request methods accept a context.Context and return (statusCode int, err error).

Usage:

import (
    "context"
    "time"
    "github.com/rahmadafandi/fibr/v2/httpclient"
)

h := httpclient.New("https://api.example.com",
    httpclient.WithTimeout(10*time.Second),
    httpclient.WithRetry(3, 500*time.Millisecond),
    httpclient.WithCircuitBreaker(5, 30*time.Second), // open after 5 consecutive failures
    httpclient.WithHeader("Authorization", "Bearer "+token),
)

var result MyResponse
code, err := h.Get(ctx, "/resource/1", &result)

code, err = h.Post(ctx, "/resource", requestBody, &result)

// Fire and forget (background, non-blocking)
h.FireAndForget(ctx, httpclient.Post, "/events", eventPayload)

WithCircuitBreaker makes the client fail fast with http.ErrCircuitOpen once a dependency has failed (transport error or 5xx) maxFailures times in a row, then lets a single probe through after the open timeout. 4xx responses do not trip it.

retry

A small generic retry helper with exponential backoff and optional jitter. Use it for any flaky operation (DB calls, third-party APIs, locks) that isn't already covered by the http client's built-in retry.

import "github.com/rahmadafandi/fibr/v2/retry"

err := retry.Do(ctx, func() error {
    return publish(ctx, msg)
}, retry.WithAttempts(5), retry.WithDelay(100*time.Millisecond), retry.WithJitter(0.2))

// With a return value, and only retrying some errors:
user, err := retry.DoValue(ctx, func() (User, error) {
    return fetchUser(ctx, id)
}, retry.WithRetryIf(func(e error) bool { return errors.Is(e, ErrTemporary) }))

Backoff is delay Γ— multiplier^attempt (capped by WithMaxDelay), jittered by WithJitter. The sleep is context-aware, so a cancelled context stops the retries promptly.

rediskit

A Redis wrapper with JSON serialization helpers and a generic Remember cache-aside function. Remember deduplicates concurrent misses for the same key within the process (singleflight), so a hot-key expiry triggers one loader call rather than a stampede.

Usage:

import (
    "context"
    "time"
    "github.com/redis/go-redis/v9"
    "github.com/rahmadafandi/fibr/v2/rediskit"
)

opt, err := rediskit.ParseRedisOptions("redis://localhost:6379/0")
if err != nil {
    // handle invalid URL
}
rds := rediskit.New(redis.NewClient(opt))

// Set / Get
_ = rds.Set(ctx, "key", myValue, time.Minute)
_ = rds.Get(ctx, "key", &myValue)

// Cache-aside: returns cached value or calls loader on miss
result, err := rediskit.Remember(ctx, rds, "key", time.Minute, func() (MyType, error) {
    return expensiveLoad()
})

Invalidate and inspect entries with Delete(ctx, keys...), Exists(ctx, key), Expire(ctx, key, ttl), and TTL(ctx, key).

Redis-backed rate limiting
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
app := bootstrap.New(bootstrap.Options{
    RateLimit:        100,
    RateLimitStorage: rediskit.NewStorage(client), // shared across instances
})
slug

Generates a unique, URL-safe slug for a given table using a Bun database.

Usage:

import (
    "context"
    "github.com/rahmadafandi/fibr/v2/slug"
)

// Returns e.g. "my-first-post-abc123defgh456"
s, err := slug.Generate(ctx, db, "posts", "My First Post")
uploader

A helper package for uploading files to local storage. NewLocalUploader accepts functional options for max file size and MIME type allowlist. The filename is sanitized automatically.

Usage:

import "github.com/rahmadafandi/fibr/v2/uploader"

// Create a local uploader (max 5 MB, images only)
up := uploader.NewLocalUploader("./uploads",
    uploader.WithMaxSize(5<<20),
    uploader.WithAllowedMime([]string{"image/jpeg", "image/png"}),
)

// Upload a file (filename is sanitized before saving)
path, err := up.Upload(ctx, file, filename) // e.g. ./uploads/3f9c2a1b7d4e6f80-photo.png

Each file is stored under a unique name, <random>-<name>, so two users uploading avatar.png never overwrite each other. Pass uploader.WithOriginalNames() to keep the original name instead; an upload then replaces an existing file of that name, so use it only for names you control. ctx bounds the upload, so it can be cancelled or given a deadline.

S3-compatible uploads with uploader.S3Uploader
client, _ := minio.New(endpoint, &minio.Options{Creds: creds, Secure: true})
up := uploader.NewS3Uploader(client, "my-bucket",
    uploader.WithKeyPrefix("avatars/"),
    uploader.WithBaseURL("https://cdn.example.com/"),
)
url, err := up.Upload(ctx, file, "photo.png") // -> https://cdn.example.com/avatars/<random>-photo.png
middleware

A collection of useful middleware.

Usage:

import "github.com/rahmadafandi/fibr/v2/middleware"

app := fiber.New()

// Recover from panics
app.Use(middleware.Recover(logger))

// Log requests
app.Use(middleware.RequestLogger(logger))

// Protect routes (JWT bearer + revocation/scopes)
app.Use(auth.RequireAuth(secret))

// Context
app.Use(middleware.ContextMiddleware(10 * time.Second))
reqctx

Accessors for values stored on the Fiber context: the request-scoped context.Context, the request ID, and type-safe locals.

Usage:

import "github.com/rahmadafandi/fibr/v2/reqctx"

ctx := reqctx.GetContext(c)        // request-scoped context.Context
id := reqctx.GetRequestID(c)       // request id (set by ContextMiddleware)

reqctx.SetLocal(c, "user", user)   // store a typed value
u := reqctx.GetLocal[User](c, "user") // retrieve it (zero value if absent)
database

Opens a Bun database, picking the dialect from the DSN (postgres:// β†’ Postgres, file:/:memory:/path β†’ SQLite).

import "github.com/rahmadafandi/fibr/v2/database"

db, err := database.NewBun("postgres://localhost/app",
    database.WithMaxOpenConns(20),
    database.WithPingTimeout(3*time.Second),
)
dbresolver

An explicit read/write split for read-replica deployments: writes go to the primary, reads are spread round-robin across replicas. Routing is explicit (you choose Reader() or Writer()) β€” Bun's query hooks are observational and can't redirect a query's connection, so transparent auto-routing isn't offered.

import "github.com/rahmadafandi/fibr/v2/dbresolver"

primary, _ := database.NewBun("postgres://primary/app")
r1, _ := database.NewBun("postgres://replica1/app")
r2, _ := database.NewBun("postgres://replica2/app")

dbr := dbresolver.New(primary, r1, r2)
defer dbr.Close()

// Writes + read-after-write:
_, _ = dbr.Writer().NewInsert().Model(&user).Exec(ctx)
// Reads (round-robin across replicas):
err := dbr.Reader().NewSelect().Model(&users).Scan(ctx)

With no replicas, Reader() returns the primary, so the same code works in single-DB setups.

migrate

Versioned database migrations over bun/migrate. Declare a collection, register Go migrations, and run them from your app binary.

// internal/migrations/migrations.go
var Migrations = migrate.NewMigrations(migrate.WithMigrationsDirectory("internal/migrations"))

// cmd/api/main.go
root.AddCommand(migrate.NewCommand(openDB, migrations.Migrations))

migrate.NewCommand gives up, down, status, and create <name> subcommands. The core funcs Up/Down/Status/Create are also usable directly. Each migration file registers itself in init() via Migrations.MustRegister(up, down); the version comes from the filename (<timestamp>_<name>.go).

auth

JWT bearer authentication and bcrypt password hashing for Fiber.

hash, _ := auth.Hash(password)          // bcrypt
err := auth.Compare(hash, password)     // nil = match

app.Get("/me", auth.RequireAuth(secret), handler)            // 401 if no/invalid token
app.Get("/admin", auth.RequireAuth(secret),
    auth.RequireScope("admin"), handler)                     // 403 if scope missing

claims, ok := auth.Claims(c)            // jwt.MapClaims stored by the middleware
sub := auth.Subject(c)                  // claims["sub"]
scopes := auth.Scopes(c)                // normalized []string

Optional(secret) validates a token when present but never rejects. Scopes are a scopes JWT claim ([]string). Tokens are minted with the jwt package (jwt.GenerateTokenWithExpiry).

Tokens without an exp claim are rejected, since they would never expire; pass auth.WithExpirationOptional() to RequireAuth/Optional to accept them. With an empty secret every request is rejected.

health

Liveness (/livez) and readiness (/readyz) endpoints with concurrent checks.

import "github.com/rahmadafandi/fibr/v2/health"

gate := health.NewReadinessGate() // fails /readyz once closed (during drain)
health.Register(app,
    health.PingBun(db),
    health.PingRedis(redisClient),
    health.PingHTTP("payments", "https://payments.internal/healthz"),
    gate.Check(),
)
// GET /livez  -> 200 {"status":"ok"}
// GET /readyz -> 200/503 {"status":"...","checks":{...}}

Built-in dependency probes: PingBun, PingRedis, PingHTTP, PingTCP. For zero-downtime rollouts, pair ReadinessGate with server.RunGracefulWithConfig so /readyz flips to not-ready and waits a drain delay before the server stops:

server.RunGracefulWithConfig(app, ":3000", server.Config{
    Timeout:     15 * time.Second,
    DrainDelay:  5 * time.Second,
    PreShutdown: []func(context.Context) error{func(context.Context) error { gate.Close(); return nil }},
    Cleanup:     []func(context.Context) error{func(context.Context) error { return db.Close() }},
})
metrics

Prometheus request metrics. Standalone:

import "github.com/rahmadafandi/fibr/v2/metrics"

app.Use(metrics.Middleware())
app.Get("/metrics", metrics.Handler())

Records http_requests_total{method,path,status} and http_request_duration_seconds{...}. The path label is the Fiber route template (e.g. /items/:id), so cardinality stays bounded. The default registry also exposes Go-runtime and process collectors (go_goroutines, GC, memory, fds). The middleware skips its own /metrics path.

Via bootstrap, enable with Options{Metrics: true} β€” it installs the middleware and registers /metrics ahead of the rate limiter. In a generated app, set METRICS_ENABLED=true.

tracing

OpenTelemetry distributed tracing. Set up the provider once at startup and defer its shutdown:

import "github.com/rahmadafandi/fibr/v2/tracing"

shutdown, err := tracing.Setup(ctx, tracing.WithServiceName("my-svc"))
if err != nil { /* handle */ }
defer shutdown(context.Background())

Setup builds an OTLP/HTTP exporter (configured by the standard OTEL_ env vars like OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_SERVICE_NAME) and installs the global tracer provider + W3C propagator. Via bootstrap, enable with Options{Tracing: true} (installs Fiber's OpenTelemetry server-span middleware) and pass shutdown through Options{Cleanup: []func(context.Context) error{shutdown}} for graceful shutdown. When tracing is active, RequestLogger adds trace_id / span_id to request logs. In a generated app, set TRACING_ENABLED=true.

When tracing is enabled, generated apps also install Bun's bunotel query hook (database.WithTracing()), so each SQL query becomes a span nested under the request span.

jobs

Redis-backed background jobs built on asynq.

import "github.com/rahmadafandi/fibr/v2/jobs"

opt, _ := jobs.RedisConnOpt(os.Getenv("REDIS_URL"))

// enqueue (HTTP side)
client := jobs.NewClient(opt)
client.Enqueue(ctx, "welcome:send", WelcomePayload{Email: "a@b.com"})

// process (worker side)
srv := jobs.NewServer(opt, jobs.ServerConfig{Concurrency: 10})
jobs.Handle[WelcomePayload](srv, "welcome:send", handleWelcome)
srv.Run()

Enqueue JSON-marshals the payload; the generic Handle[T] decodes it back into T before calling your handler (a malformed payload wraps asynq.SkipRetry so it is not retried forever). Mount the asynqmon dashboard through bootstrap (the asynqmon dependency stays out of bootstrap itself):

bootstrap.New(bootstrap.Options{
    Asynqmon: &bootstrap.AsynqmonMount{
        Handler: jobs.MonitoringHandler(opt, "/monitoring"),
        Path:    "/monitoring",
    },
})

Generate an app with the queue scaffolded via fibr new --queue: it adds a worker subcommand, a sample job, the monitoring UI mount, and the REDIS_URL / QUEUE_CONCURRENCY / ASYNQMON_PATH config keys. When REDIS_URL is unset the queue is disabled with a startup warning (the worker subcommand exits with an error).

Scheduled jobs with jobs.Scheduler
sched := jobs.NewScheduler(opt) // opt from jobs.RedisConnOpt(redisURL)
if _, err := sched.Register("0 2 * * *", "cleanup:run", CleanupPayload{OlderThanDays: 30}); err != nil {
    log.Fatal(err)
}
log.Fatal(sched.Run()) // run ONE instance; workers process the enqueued tasks
lock

A single-instance Redis distributed mutex. Across multiple replicas, it guarantees that a unit of work runs on at most one of them at a time β€” for example, so a jobs.Scheduler cron task does not fire once per replica.

locker := lock.New(redisClient) // redisClient is a redis.UniversalClient

// Run-once across replicas: acquires, runs fn, releases. Returns
// lock.ErrNotAcquired (without running fn) if another replica holds it.
err := locker.Do(ctx, "cron:nightly-cleanup", 30*time.Second, func() error {
    return cleanup(ctx)
})
if err != nil && !errors.Is(err, lock.ErrNotAcquired) {
    log.Fatal(err)
}

For finer control: TryAcquire (one non-blocking attempt), Acquire (blocks until the lock is free or the context ends), and on the returned handle Extend (renew the TTL for long-running work) and Release. Release and Extend are owner-only β€” a token guards against deleting or renewing a lock another replica has since taken over.

outbox

A transactional outbox: write an event into the outbox table in the same DB transaction as your business data, then let a background relay publish it. This avoids the dual-write problem β€” you can't atomically commit to the database and publish to Redis, so instead the event is committed with your data and published afterwards (at-least-once; make consumers idempotent).

// Once, at startup:
_ = outbox.Migrate(ctx, db)

// In your handler/service, within the business transaction:
err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
    if _, err := tx.NewInsert().Model(&order).Exec(ctx); err != nil {
        return err
    }
    return outbox.Enqueue(ctx, tx, "order.created", OrderCreated{ID: order.ID})
})

// In a background worker (run one logical relay; pass a lock.Locker to make it
// safe to run on every replica β€” only one relays at a time):
relay := outbox.NewRelay(db, outbox.NewRedisPublisher(redisClient),
    outbox.WithLock(lock.New(redisClient), "outbox:relay", 30*time.Second),
    outbox.WithMaxAttempts(5), // dead-letter a poison event instead of blocking the queue
    outbox.WithLogger(log))
go relay.Run(ctx)

// On a schedule (e.g. a jobs.Scheduler task): drop events published over a week ago.
_, _ = outbox.Prune(ctx, db, 7*24*time.Hour)

Consumers subscribe with rediskit.Subscribe[OrderCreated](...) β€” the relay publishes the stored JSON bytes verbatim, so they decode directly.

  • Ordering and failures. Events are published oldest first, and a publish error stops the batch so order is preserved β€” which means one event that can never be published blocks everything behind it. WithMaxAttempts(n) moves such an event to the outbox_dead table (outbox.DeadEvent, with the error) after n consecutive failures and carries on. The attempt count is kept in memory, per relay process. Migrate creates outbox_dead; run it again after upgrading.
  • Locking. With WithLock, the relay renews the lock before every event, so the TTL only needs to exceed the time to publish one event, not a whole batch.
  • Retention. Published rows are kept until you call outbox.Prune.
inbox

The consumer-side complement to outbox: at-least-once delivery means a message can arrive twice, so the consumer must dedup. Once records a marker and runs your handler in a single transaction β€” a duplicate message id skips the handler, and a handler error rolls the marker back so the message is retried later.

import "github.com/rahmadafandi/fibr/v2/inbox"

_ = inbox.Migrate(ctx, db) // once at startup

// In your message/event handler:
err := inbox.Once(ctx, db, msg.ID, func(ctx context.Context, tx bun.Tx) error {
    // Do the work through tx so it commits atomically with the dedup marker.
    _, err := tx.NewInsert().Model(&order).Exec(ctx)
    return err
})

Pair it with outbox (producer) for end-to-end exactly-once-effect processing over an at-least-once transport. Markers are kept until you purge them with inbox.Prune(ctx, db, olderThan) β€” keep them at least as long as a message can still be redelivered.

events

An in-process typed event bus for decoupling domain-event producers from handlers. Handlers are keyed by event type; Publish is synchronous by default (handlers run inline, errors joined) with an opt-in async mode. For durable, cross-process delivery use outbox instead.

import "github.com/rahmadafandi/fibr/v2/events"

type OrderCreated struct{ ID int }

bus := events.New() // or events.New(events.WithAsync())
events.Subscribe(bus, func(ctx context.Context, e OrderCreated) error {
    return mailer.SendOrderConfirmation(ctx, e.ID)
})

// Producer β€” runs handlers now, returns their joined errors (sync mode).
if err := events.Publish(ctx, bus, OrderCreated{ID: order.ID}); err != nil {
    log.Error(err, "order.created handlers failed")
}
featureflag

Runtime feature flags with pluggable providers: Static (on/off map), Rules (boolean + percentage rollout + per-user/group allowlists), and Redis (toggle live without a redeploy). Percentage rollout buckets users by a stable hash, so a user's membership doesn't flicker between checks.

import "github.com/rahmadafandi/fibr/v2/featureflag"

flags := featureflag.New(featureflag.Rules(map[string]featureflag.Rule{
    "new_checkout": {Percentage: 25, Users: []string{"vip"}},
}))

// Direct evaluation:
if flags.Enabled(ctx, "new_checkout", featureflag.Eval{UserID: uid}) { ... }

// Or via Fiber: derive the eval context from the request, then check per-handler.
app.Use(flags.Middleware(func(c fiber.Ctx) featureflag.Eval {
    return featureflag.Eval{UserID: auth.UserID(c)}
}))
app.Get("/checkout", func(c fiber.Ctx) error {
    if featureflag.Enabled(c, "new_checkout") {
        return newCheckout(c)
    }
    return oldCheckout(c)
})

With the Redis provider, each flag is a key (prefix+flag) holding "true"/"false" or a JSON Rule, so ops can flip flags without a deploy.

audit

An append-only audit trail of who did what. A Recorder writes Entry records (actor, action, target, metadata, IP, request id) through a pluggable Sink; the built-in Bun sink persists them and List reads them back.

import "github.com/rahmadafandi/fibr/v2/audit"

_ = audit.Migrate(ctx, db) // once at startup

rec := audit.New(audit.NewBunSink(db), audit.WithActor(func(c fiber.Ctx) string {
    return auth.Subject(c) // who is acting
}))

app.Delete("/orders/:id", func(c fiber.Ctx) error {
    if err := orders.Delete(c.Context(), c.Params("id")); err != nil {
        return err
    }
    e := rec.FromRequest(c) // prefills actor, IP, request id
    e.Action, e.Target, e.TargetID = "order.delete", "order", c.Params("id")
    _ = rec.Record(c.Context(), e)
    return c.SendStatus(fiber.StatusNoContent)
})

// Read back:
entries, _ := audit.List(ctx, db, audit.Filter{Actor: "u1", Limit: 50})

Record is synchronous; for hot paths route it through outbox or a goroutine. Implement Sink to send entries elsewhere (e.g. a SIEM) instead of the database.

cache

A generic in-process cache with per-entry TTL, LRU max-size eviction, and singleflight load-through. Use it for hot data (config, feature flags, lookups) where a Redis round-trip per read is overkill; rediskit.Remember remains the choice for a cache shared across instances.

import "github.com/rahmadafandi/fibr/v2/cache"

c := cache.New[User](
    cache.WithMaxSize(10_000),
    cache.WithDefaultTTL(time.Minute),
    cache.WithJanitor(time.Minute), // optional background sweep; call c.Close() to stop
)

// Load-through: runs the loader once on a miss even under concurrent callers.
u, err := c.GetOrLoad(ctx, "user:1", func() (User, error) {
    return repo.FindUser(ctx, 1)
})

c.Set("k", v); v, ok := c.Get("k"); c.Delete("k")

Cache[V] is type-safe (no any casts at call sites). Eviction is count-based LRU; expiry is lazy (checked on Get) plus an optional janitor.

ratelimit

A Redis-backed token-bucket limiter: each key has a bucket of Capacity tokens that refills at RefillPerSec, and a request consumes cost tokens. Unlike Fiber's window limiter, it supports cost-per-request and arbitrary per-key rules β€” good for API quotas where some endpoints are "more expensive". The refill is computed atomically in a Lua script, so it's correct across instances.

import "github.com/rahmadafandi/fibr/v2/ratelimit"

l := ratelimit.New(redisClient)
rule := ratelimit.Rule{Capacity: 100, RefillPerSec: 10} // burst 100, sustain 10/s

// Direct:
res, err := l.Allow(ctx, "user:"+uid, rule, 1)
if !res.Allowed { /* res.RetryAfter, res.Remaining */ }

// Fiber middleware β€” 429 + Retry-After + X-RateLimit-* on deny:
app.Use(l.Middleware(rule, func(c fiber.Ctx) string { return c.IP() }))

A heavier endpoint can charge more: l.Allow(ctx, key, rule, 5). On a Redis error the middleware fails open (allows the request) so the limiter can't take the app down; the error is logged to logger.Default(), or to your own logger with ratelimit.New(redisClient, ratelimit.WithLogger(log)).

apikey

API-key authentication, distinct from the JWT-bearer auth package β€” for service-to-service calls and public APIs. A presented key is hashed (SHA-256) and resolved to an identity + scopes via a pluggable Store; raw keys are never stored.

import "github.com/rahmadafandi/fibr/v2/apikey"

// Issue a key: hand `key` to the client, persist `hash`.
key, hash, _ := apikey.Generate()

a := apikey.New(apikey.Config{Store: apikey.MapStore(map[string]apikey.Identity{
    hash: {ID: "service-a", Scopes: []string{"read"}},
})})

app.Use(a.Middleware())                       // reads X-API-Key β†’ 401 or sets identity
app.Get("/data", a.RequireScope("read"), handler)

app.Get("/whoami", func(c fiber.Ctx) error {
    id, _ := apikey.FromContext(c)
    return c.JSON(id) // {ID, Scopes, Meta}
})

MapStore covers static config and tests; back it with a DB or Redis by implementing Store (Lookup(ctx, keyHash) (*Identity, error)). The middleware returns apierror values, so with bootstrap (or apierror.Handler) failures render as the standard JSON envelope.

mailer

Transactional email through a pluggable Sender.

import "github.com/rahmadafandi/fibr/v2/mailer"

sender, _ := mailer.New(mailer.SMTPConfig{
    Host: os.Getenv("SMTP_HOST"), Port: 587,
    Username: os.Getenv("SMTP_USERNAME"), Password: os.Getenv("SMTP_PASSWORD"),
    From: "no-reply@example.com",
})

html, text, _ := mailer.Render("<p>Hi {{.Name}}</p>", "Hi {{.Name}}", data)
sender.Send(ctx, mailer.Message{To: []string{"a@b.com"}, Subject: "Hi", HTML: html, Text: text})

New returns an SMTP sender when Host is set, otherwise a LogSender that logs instead of sending (handy in development). A MemorySender captures messages for tests. Because Message is JSON-serializable it doubles as an asynq job payload β€” generated apps with both --mailer and --queue send asynchronously through an email:send job; with --mailer alone they send inline. fibr new --mailer also sends the team invitation email (with --auth-with-team) and the welcome job (with --queue).

server

Signal-based graceful shutdown.

import "github.com/rahmadafandi/fibr/v2/server"

err := server.RunGraceful(app, ":3000", 10*time.Second, func(ctx context.Context) error {
    return db.Close()
})

A zero (or negative) timeout falls back to 10 seconds.

Typed errors with apierror

apierror.NotFound("..."), Conflict, Unauthorized, ... return typed *Error values. bootstrap installs apierror.Handler as the default ErrorHandler, so returning one from a handler renders a consistent JSON error.

func getUser(c fiber.Ctx) error {
    u, err := svc.Find(c.Context(), id)
    if err != nil {
        return apierror.NotFound("user not found").WithCode("user_not_found")
    }
    return response.SendSuccess(c, u, "ok")
}
// bootstrap.New installs apierror.Handler, so the return renders as:
// {"code":404,"message":"user not found","error":"user_not_found","status":"error"}

apierror.StatusOf(err) returns the status Handler will respond with. The request logger and the metrics middleware use it, so a returned error is logged and counted under its real status (e.g. 404) rather than the not-yet-written response code.

bootstrap

Optional one-call wiring of recover, request id, logging, optional CORS / rate limit / health, and graceful shutdown.

import "github.com/rahmadafandi/fibr/v2/bootstrap"

app := bootstrap.New(bootstrap.Options{
    DB:           db,
    CORSOrigins:  []string{"https://app.example.com"},
    RateLimit:    100,
    HealthChecks: []health.NamedCheck{health.PingBun(db)},
})
app.Get("/", handler)
log.Fatal(app.Run(":3000")) // graceful shutdown + db.Close handled

CORS is off unless you ask for it. CORSOrigins enables it for exactly the listed origins; for anything beyond that (credentials, headers, methods), pass a full config:

bootstrap.Options{
    CORS: &cors.Config{AllowOrigins: []string{"https://app.example.com"}, AllowCredentials: true},
}

Inside a handler, c.Context() and reqctx.GetContext(c) return the same request context: bounded by RequestTimeout, carrying the request id, and β€” with Tracing: true β€” the request span, so database calls made with it show up under the request's trace.

fibrtest

A test harness that removes the httptest / app.Test / decode / assert boilerplate from handler tests. A fluent client drives the app; responses assert their own status and decode JSON.

import "github.com/rahmadafandi/fibr/v2/fibrtest"

func TestCreateThing(t *testing.T) {
    app := bootstrap.New(bootstrap.Options{DB: fibrtest.NewDB(t)})
    app.Post("/things", createThing)

    c := fibrtest.New(t, app.App).
        WithBearer(fibrtest.Token(t, secret, jwt.MapClaims{"sub": "u1"}))

    var out Thing
    c.Post("/things", ThingReq{Name: "x"}).ExpectStatus(201).JSON(&out)
    assert.Equal(t, "x", out.Name)
}

New takes any *testing.T (via a minimal TB interface). Use the Get/Post/Put/Patch/Delete shortcuts, or Request(method, path) for a builder with JSON/Body/Header/Bearer/Query. NewDB returns a throwaway in-memory SQLite Bun DB (auto-closed) and Token mints a signed JWT for authed routes.

Modules

A Module is a self-contained feature that registers its own routes. Mount it in one line; it can optionally migrate its tables and report health.

type Module interface {
    Name() string
    Register(r fiber.Router) error
}
// optional, detected via type assertion:
type Migrator      interface{ Migrate(ctx context.Context) error }
type HealthChecker interface{ Checks() []health.NamedCheck }

app := bootstrap.New(bootstrap.Options{DB: db})
if err := app.Mount(user.NewUserModule(db), product.NewProductModule(db)); err != nil {
    log.Fatal(err)
}

By default Mount does not create tables β€” use migrations (migrate.NewCommand). Set bootstrap.Options{AutoMigrate: true} (e.g. from an AUTO_MIGRATE env in dev) to have Mount run each module's Migrate at startup.

Mount registers each module's routes and, when AutoMigrate is enabled, runs its Migrate. It also adds its Checks() to /readyz.

Note: *bootstrap.App.Mount is the module-aware method and shadows Fiber's Mount. Where a fiber.Router is needed (e.g. passing the app to a route registrar), use the embedded app.App.

License

MIT Β© 2026 Rahmad Afandi

Directories ΒΆ

Path Synopsis
Package apierror provides typed HTTP errors and a Fiber ErrorHandler that renders them (and *fiber.Error, and unknown errors) as the standard JSON envelope.
Package apierror provides typed HTTP errors and a Fiber ErrorHandler that renders them (and *fiber.Error, and unknown errors) as the standard JSON envelope.
Package apikey provides API-key authentication for Fiber, distinct from the JWT-bearer auth package: a presented key is hashed and looked up in a pluggable store to resolve its identity and scopes.
Package apikey provides API-key authentication for Fiber, distinct from the JWT-bearer auth package: a presented key is hashed and looked up in a pluggable store to resolve its identity and scopes.
Package audit records an append-only actor/action/target trail through a pluggable Sink, with a Bun-backed sink and a Fiber helper that prefills request context.
Package audit records an append-only actor/action/target trail through a pluggable Sink, with a Bun-backed sink and a Fiber helper that prefills request context.
Package auth provides bcrypt password hashing and JWT bearer authentication middleware (with scope checks) for Fiber apps, built on the jwt, context, and response packages.
Package auth provides bcrypt password hashing and JWT bearer authentication middleware (with scope checks) for Fiber apps, built on the jwt, context, and response packages.
Package bind parses and validates request input in one call.
Package bind parses and validates request input in one call.
Package bootstrap wires a Fiber app β€” middleware, health, metrics, tracing, OpenAPI docs, i18n, and graceful shutdown β€” from a single Options struct.
Package bootstrap wires a Fiber app β€” middleware, health, metrics, tracing, OpenAPI docs, i18n, and graceful shutdown β€” from a single Options struct.
Package cache is a generic in-process cache with TTL, LRU max-size eviction, and singleflight load deduplication.
Package cache is a generic in-process cache with TTL, LRU max-size eviction, and singleflight load deduplication.
cmd
fibr command
Package config loads environment variables into typed structs using default and required struct tags.
Package config loads environment variables into typed structs using default and required struct tags.
Package database connects to Postgres or SQLite via Bun, with dialect auto-detection and optional tracing.
Package database connects to Postgres or SQLite via Bun, with dialect auto-detection and optional tracing.
Package dbresolver provides an explicit read/write split over Bun: writes go to the primary, reads are spread round-robin across replicas.
Package dbresolver provides an explicit read/write split over Bun: writes go to the primary, reads are spread round-robin across replicas.
Package events provides a generic, type-keyed in-process event bus so domain event producers and handlers are decoupled within a process.
Package events provides a generic, type-keyed in-process event bus so domain event producers and handlers are decoupled within a process.
Package featureflag evaluates runtime feature flags (boolean, percentage rollout, and per-user/group targeting) through a pluggable provider, with built-in Static, Rules, and Redis providers and a Fiber helper.
Package featureflag evaluates runtime feature flags (boolean, percentage rollout, and per-user/group targeting) through a pluggable provider, with built-in Static, Rules, and Redis providers and a Fiber helper.
Package fibrtest provides test helpers for fibr/Fiber apps: a fluent HTTP client over *fiber.App, response assertions, a JWT minting helper, and an in-memory Bun DB helper.
Package fibrtest provides test helpers for fibr/Fiber apps: a fluent HTTP client over *fiber.App, response assertions, a JWT minting helper, and an in-memory Bun DB helper.
Package health exposes liveness and readiness HTTP endpoints backed by named checks.
Package health exposes liveness and readiness HTTP endpoints backed by named checks.
Package httpclient is a context-aware JSON HTTP client with retries.
Package httpclient is a context-aware JSON HTTP client with retries.
Package i18n is a small, dependency-free internationalization helper: a message catalog (Bundle) loaded from nested JSON, with {placeholder} substitution, one/other pluralization, and a fallback locale.
Package i18n is a small, dependency-free internationalization helper: a message catalog (Bundle) loaded from nested JSON, with {placeholder} substitution, one/other pluralization, and a fallback locale.
Package inbox implements the idempotent-consumer pattern: a dedup table that lets a consumer process each message exactly once under at-least-once delivery.
Package inbox implements the idempotent-consumer pattern: a dedup table that lets a consumer process each message exactly once under at-least-once delivery.
Package jobs provides a thin, typed wrapper over hibiken/asynq for enqueuing and processing background jobs, plus a mountable asynqmon monitoring handler.
Package jobs provides a thin, typed wrapper over hibiken/asynq for enqueuing and processing background jobs, plus a mountable asynqmon monitoring handler.
Package jwt generates and validates JSON Web Tokens.
Package jwt generates and validates JSON Web Tokens.
Package lock provides a single-instance Redis distributed mutex with owner-only release and TTL extension, for single-execution across replicas.
Package lock provides a single-instance Redis distributed mutex with owner-only release and TTL extension, for single-execution across replicas.
Package logger is a structured logger built on zerolog.
Package logger is a structured logger built on zerolog.
Package mailer sends transactional email through a pluggable Sender.
Package mailer sends transactional email through a pluggable Sender.
Package metrics provides Prometheus request metrics for Fiber apps: a middleware that records request count and latency, and a handler that serves the Prometheus exposition format.
Package metrics provides Prometheus request metrics for Fiber apps: a middleware that records request count and latency, and a handler that serves the Prometheus exposition format.
Package middleware provides Fiber middleware: recover, request logging, and request IDs.
Package middleware provides Fiber middleware: recover, request logging, and request IDs.
Package migrate provides thin, testable helpers over bun/migrate plus a ready cobra command for running migrations from an application binary.
Package migrate provides thin, testable helpers over bun/migrate plus a ready cobra command for running migrations from an application binary.
Package openapi builds an OpenAPI 3.0.3 document from runtime-registered routes and reflected request/response structs, and serves it together with a CDN-backed Swagger UI.
Package openapi builds an OpenAPI 3.0.3 document from runtime-registered routes and reflected request/response structs, and serves it together with a CDN-backed Swagger UI.
Package outbox implements the transactional-outbox pattern: events are written into an outbox table within the same database transaction as the business data, and a background Relay publishes pending events at-least-once.
Package outbox implements the transactional-outbox pattern: events are written into an outbox table within the same database transaction as the business data, and a background Relay publishes pending events at-least-once.
Package pagination provides a paginated result envelope with page metadata.
Package pagination provides a paginated result envelope with page metadata.
Package parser builds Bun pagination and search query modifiers from request query parameters.
Package parser builds Bun pagination and search query modifiers from request query parameters.
Package ratelimit is a Redis-backed token-bucket rate limiter with per-key buckets, cost-per-request, and a Fiber middleware.
Package ratelimit is a Redis-backed token-bucket rate limiter with per-key buckets, cost-per-request, and a Fiber middleware.
Package rediskit wraps go-redis with cache-aside, a fiber.Storage adapter, pub/sub, and bulk/atomic helpers.
Package rediskit wraps go-redis with cache-aside, a fiber.Storage adapter, pub/sub, and bulk/atomic helpers.
Package reqctx provides request-scoped helpers: context access, request IDs, and type-safe locals.
Package reqctx provides request-scoped helpers: context access, request IDs, and type-safe locals.
Package response writes standardized JSON success and error envelopes.
Package response writes standardized JSON success and error envelopes.
Package retry runs a function with configurable retries, exponential backoff, and optional jitter.
Package retry runs a function with configurable retries, exponential backoff, and optional jitter.
Package server runs a Fiber app with signal-based graceful shutdown.
Package server runs a Fiber app with signal-based graceful shutdown.
Package slug generates unique URL-safe slugs backed by a Bun database.
Package slug generates unique URL-safe slugs backed by a Bun database.
Package sse serves Server-Sent Events: a one-way text/event-stream from server to client.
Package sse serves Server-Sent Events: a one-way text/event-stream from server to client.
Package tracing wires OpenTelemetry distributed tracing for Fiber apps: it builds an OTLP/HTTP tracer provider (configured by the standard OTEL_ env vars), installs it and the W3C propagator as the OTel globals, and returns a shutdown function for graceful shutdown.
Package tracing wires OpenTelemetry distributed tracing for Fiber apps: it builds an OTLP/HTTP tracer provider (configured by the standard OTEL_ env vars), installs it and the W3C propagator as the OTel globals, and returns a shutdown function for graceful shutdown.
Package uploader stores uploaded multipart files to a local directory or an S3-compatible bucket behind a common Uploader interface.
Package uploader stores uploaded multipart files to a local directory or an S3-compatible bucket behind a common Uploader interface.
Package validator validates structs using go-playground/validator with JSON field names.
Package validator validates structs using go-playground/validator with JSON field names.
Package webhook signs and verifies webhook payloads with an HMAC-SHA256 scheme: the signature header is "t=<unix>,v1=<hex>" where the MAC is computed over "<unix>.<payload>".
Package webhook signs and verifies webhook payloads with an HMAC-SHA256 scheme: the signature header is "t=<unix>,v1=<hex>" where the MAC is computed over "<unix>.<payload>".
Package ws provides a typed WebSocket hub with rooms and JSON broadcast on top of github.com/gofiber/contrib/websocket.
Package ws provides a typed WebSocket hub with rooms and JSON broadcast on top of github.com/gofiber/contrib/websocket.

Jump to

Keyboard shortcuts

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