{"id":51094981,"url":"https://github.com/rahmadafandi/fibr","last_synced_at":"2026-06-24T05:32:35.601Z","repository":{"id":311170846,"uuid":"1042671566","full_name":"rahmadafandi/fibr","owner":"rahmadafandi","description":"Reusable Fiber (Go) helper packages + the fibr generator — auth \u0026 teams, background jobs, mailer, metrics, tracing, migrations, and more.","archived":false,"fork":false,"pushed_at":"2026-06-09T05:38:56.000Z","size":4250,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-09T07:24:46.967Z","etag":null,"topics":["asynq","boilerplate","bun","code-generator","fiber","go","gofiber","golang","jwt-authentication","opentelemetry","prometheus","rest-api","scaffolding","web-framework"],"latest_commit_sha":null,"homepage":"https://rahmadafandi.github.io/fibr/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rahmadafandi.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-22T11:42:38.000Z","updated_at":"2026-06-09T05:38:51.000Z","dependencies_parsed_at":"2025-08-22T16:03:24.528Z","dependency_job_id":"e1db21df-b6fc-4c16-92cd-d421d31e0ac8","html_url":"https://github.com/rahmadafandi/fibr","commit_stats":null,"previous_names":["rahmadafandi/fiber-helpers","rahmadafandi/fibr"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/rahmadafandi/fibr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rahmadafandi%2Ffibr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rahmadafandi%2Ffibr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rahmadafandi%2Ffibr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rahmadafandi%2Ffibr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rahmadafandi","download_url":"https://codeload.github.com/rahmadafandi/fibr/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rahmadafandi%2Ffibr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34719097,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-24T02:00:07.484Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asynq","boilerplate","bun","code-generator","fiber","go","gofiber","golang","jwt-authentication","opentelemetry","prometheus","rest-api","scaffolding","web-framework"],"created_at":"2026-06-24T05:32:34.416Z","updated_at":"2026-06-24T05:32:35.558Z","avatar_url":"https://github.com/rahmadafandi.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fibr\n\n[![ci](https://github.com/rahmadafandi/fibr/actions/workflows/ci.yml/badge.svg)](https://github.com/rahmadafandi/fibr/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/rahmadafandi/fibr/branch/master/graph/badge.svg)](https://codecov.io/gh/rahmadafandi/fibr)\n[![Go Reference](https://pkg.go.dev/badge/github.com/rahmadafandi/fibr.svg)](https://pkg.go.dev/github.com/rahmadafandi/fibr)\n[![Go Report Card](https://goreportcard.com/badge/github.com/rahmadafandi/fibr)](https://goreportcard.com/report/github.com/rahmadafandi/fibr)\n[![Release](https://img.shields.io/github/v/release/rahmadafandi/fibr)](https://github.com/rahmadafandi/fibr/releases/latest)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\nA collection of helper packages for the [Fiber](https://gofiber.io/) web framework.\n\n📖 **[Documentation](https://rahmadafandi.github.io/fibr/)** · [API reference (pkg.go.dev)](https://pkg.go.dev/github.com/rahmadafandi/fibr)\n\n## Install\n\n```bash\ngo get github.com/rahmadafandi/fibr\n```\n\nRequires Go 1.26+. Targets Fiber v2 and Bun ORM (Postgres or SQLite).\n\n## Stability\n\nfibr follows [Semantic Versioning](https://semver.org/). As of **v1.0.0** the\npublic API is stable:\n\n- **No breaking changes within v1.x.** Exported identifiers will not be removed\n  or changed incompatibly until a v2 major release.\n- New functionality arrives in backward-compatible **minor** releases; fixes in\n  **patch** releases.\n- Anything breaking is deferred to v2 (a new module path, `…/fibr/v2`).\n- The generated-app scaffolding (`fibr new`) follows the same library version but\n  generated code is a starting point you own — regenerating is never required.\n\nNot covered by the guarantee: unexported APIs, behavior explicitly documented as\nexperimental, and transitive dependency internals.\n\n## Quickstart\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n\n    \"github.com/gofiber/fiber/v2\"\n    \"github.com/rahmadafandi/fibr/bootstrap\"\n    \"github.com/rahmadafandi/fibr/config\"\n    \"github.com/rahmadafandi/fibr/database\"\n    \"github.com/rahmadafandi/fibr/health\"\n    \"github.com/rahmadafandi/fibr/response\"\n)\n\nfunc main() {\n    type Config struct {\n        DatabaseURL string `mapstructure:\"DATABASE_URL\" default:\"file::memory:?cache=shared\"`\n    }\n\n    var cfg Config\n    if err := config.LoadConfig(\u0026cfg); err != nil {\n        panic(err)\n    }\n\n    db, err := database.NewBun(cfg.DatabaseURL)\n    if err != nil {\n        panic(err)\n    }\n\n    app := bootstrap.New(bootstrap.Options{\n        DB:           db,\n        EnableCORS:   true,\n        RateLimit:    100,\n        HealthChecks: []health.NamedCheck{health.PingBun(db)},\n    })\n\n    app.Get(\"/\", func(c *fiber.Ctx) error {\n        return response.SendSuccess(c, \"Hello, World!\", \"Welcome\")\n    })\n\n    fmt.Println(\"Server listening on :3000\")\n    if err := app.Run(\":3000\"); err != nil {\n        panic(err)\n    }\n}\n```\n\n## Architecture\n\nFor how the packages layer and how `bootstrap` composes them into an app, see\n[ARCHITECTURE.md](ARCHITECTURE.md).\n\n## Package Index\n\n- [`config`](#config) — Load env vars into typed structs with `default` and `required` tags.\n- [`logger`](#logger) — Structured logger based on zerolog.\n- [`response`](#response) — Standardized JSON response helpers.\n- [`parser`](#parser) — Bun pagination/search query modifiers, including keyset (cursor) pagination.\n- [`pagination`](#pagination) — Paginated result envelope with page metadata; offset (`NewPagination`) and cursor (`CursorPage`) variants.\n- [`validator`](#validator) — Struct validation with custom rules and JSON field names.\n- [`bind`](#parse--validate-with-bind) — Parse and validate a request body/query/params into `T` in one call; writes `400`/`422` on failure.\n- [`jwt`](#jwt) — JWT generation and validation helpers.\n- [`http`](#http) — Context-aware JSON HTTP client with retry.\n- [`retry`](#retry) — Generic retry with exponential backoff + jitter (`Do`/`DoValue`).\n- [`redis`](#redis) — Redis wrapper with `Remember` cache-aside helper. Includes a `Storage` adapter (`NewStorage`) for Redis-backed rate limiting.\n- [`slug`](#slug) — Unique URL-safe slug generator backed by a Bun database.\n- [`uploader`](#uploader) — Local file uploader with size and MIME limits. Also includes `S3Uploader` for S3-compatible storage (AWS S3, MinIO, R2).\n- [`middleware`](#middleware) — Recover, request logging, and request-id middleware.\n- [`context`](#context) — Request context, request-id, and type-safe local accessors.\n- [`database`](#database) — Bun connector with Postgres/SQLite dialect auto-detection.\n- [`dbresolver`](#dbresolver) — Read/write split over Bun: `Writer()` (primary) and `Reader()` (round-robin replica).\n- [`migrate`](#migrate) — Versioned migrations with `bun/migrate` and a ready cobra command.\n- [`auth`](#auth) — JWT bearer authentication and bcrypt password hashing for Fiber.\n- [`health`](#health) — Liveness (`/livez`) and readiness (`/readyz`) endpoints.\n- [`metrics`](#metrics) — Prometheus request metrics middleware + `/metrics` handler.\n- [`tracing`](#tracing) — OpenTelemetry tracing setup (OTLP/HTTP) + Fiber spans.\n- [`jobs`](#jobs) — Redis-backed background jobs (asynq) + asynqmon monitoring mount. Includes `Scheduler` for cron-triggered (periodic) jobs.\n- [`lock`](#lock) — Single-instance Redis distributed mutex (`TryAcquire`/`Acquire`/`Do`, owner-only `Release`/`Extend`) for single-execution across replicas.\n- [`outbox`](#outbox) — Transactional outbox: enqueue events in the same DB transaction as your writes; a background relay publishes them at-least-once.\n- [`events`](#events) — In-process typed event bus (`Subscribe[T]`/`Publish[T]`), sync by default with opt-in async.\n- [`inbox`](#inbox) — Idempotent-consumer dedup (`Once`); consumer-side complement to `outbox`.\n- [`featureflag`](#featureflag) — Runtime flags (boolean, percentage rollout, per-user/group) via Static/Rules/Redis providers + Fiber helper.\n- [`audit`](#audit) — Structured audit log (actor/action/target) persisted via Bun, with a Fiber request helper.\n- [`cache`](#cache) — Generic in-memory cache (`Cache[V]`) with TTL, LRU eviction, and singleflight `GetOrLoad`.\n- [`ratelimit`](#ratelimit) — Redis token-bucket rate limiter (per-key, cost-aware) + Fiber middleware.\n- [`apikey`](#apikey) — API-key auth (hashed keys, scopes, pluggable store) + Fiber middleware.\n- [`mailer`](#mailer) — Transactional email: pluggable `Sender` (SMTP/log/memory) + template render.\n- [`server`](#server) — Signal-based graceful shutdown via `RunGraceful`.\n- [`apierror`](#typed-errors-with-apierror) — Typed HTTP errors (`BadRequest`, `NotFound`, `Conflict`, ...) with a Fiber `ErrorHandler`; installed automatically by `bootstrap`.\n- [`bootstrap`](#bootstrap) — One-call app wiring: middleware, health, DB, and graceful shutdown.\n- [`fibrtest`](#fibrtest) — Test harness: fluent HTTP client over `*fiber.App`, response assertions, in-memory DB + JWT helpers.\n\n## Packages\n\n### `config`\n\nLoads 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.\n\n**Usage:**\n\n```go\nimport \"github.com/rahmadafandi/fibr/config\"\n\ntype AppConfig struct {\n    Port    int           `mapstructure:\"port\"     default:\"8080\"`\n    DBURL   string        `mapstructure:\"db_url\"   required:\"true\"`\n    Timeout time.Duration `mapstructure:\"timeout\"  default:\"30s\"`\n    Hosts   []string      `mapstructure:\"hosts\"    default:\"a,b,c\"`\n}\n\nvar cfg AppConfig\nerr := config.LoadConfig(\u0026cfg)\n```\n\n### `logger`\n\nA structured logger based on [zerolog](https://github.com/rs/zerolog).\n\n**Usage:**\n\n```go\nimport \"github.com/rahmadafandi/fibr/logger\"\n\nlog := logger.Default()\nlog.Info(\"Hello, world!\")\n```\n\n### `response`\n\nHelper functions for sending standardized JSON responses.\n\n**Usage:**\n\n```go\nimport \"github.com/rahmadafandi/fibr/response\"\n\nresponse.SendSuccess(c, data, \"Success\")\nresponse.SendError(c, nil, \"Error\", 400)\n```\n\n### `parser`\n\nHelper functions for pagination with Bun. The `Paginate` helper returns a Bun query modifier for use with `query.Apply`.\n\nRequest body/query/params parsing uses Fiber's built-in `c.BodyParser(\u0026out)`,\n`c.QueryParser(\u0026out)`, and `c.ParamsParser(\u0026out)` directly.\n\n**Usage:**\n\n```go\nimport \"github.com/rahmadafandi/fibr/parser\"\n\ntype MyStruct struct {\n    Name string `json:\"name\"`\n}\n\n// Pagination with Bun\npq := \u0026parser.PaginationQuery{}\nif err := c.QueryParser(pq); err != nil { ... }\nif err := pq.Validate([]string{\"name\", \"created_at\"}); err != nil { ... }\n\nvar rows []MyModel\nerr = db.NewSelect().Model(\u0026rows).Apply(parser.Paginate(pq, []string{\"name\"})).Scan(ctx)\n\n// Count with search filter\ncount, err := db.NewSelect().Model(\u0026rows).Apply(parser.Count(pq.Search, []string{\"name\"})).Count(ctx)\n```\n\n#### Keyset (cursor) pagination\n\nFor 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.\n\n```go\nvar kq parser.KeysetQuery // bound from ?limit=20\u0026cursor=...\u0026before=...\n_ = c.QueryParser(\u0026kq)\n\n// The columns must form a unique total ordering; make the last a tiebreaker (the pk).\ncols := []parser.KeysetColumn{{Name: \"created_at\", Desc: true}, {Name: \"id\", Desc: true}}\n\nvar rows []Article\nerr := db.NewSelect().Model(\u0026rows).Apply(parser.Keyset(kq, cols)).Scan(ctx)\n\npage := pagination.NewCursorPage(rows, kq, cols, func(a Article) []any {\n    return []any{a.CreatedAt, a.ID} // values in the same order as cols\n})\n// page.Data, page.NextCursor (\"\"=last), page.PrevCursor (\"\"=first)\n```\n\nSupported 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.\n\n### `pagination`\n\nBuilds 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`).\n\n**Usage:**\n\n```go\nimport \"github.com/rahmadafandi/fibr/pagination\"\n\np := pagination.NewPagination(rows, pq.Limit, pq.Page, totalCount)\n// p.Data, p.PageSize, p.Count, p.TotalCount, p.PageCount, p.PageNumber, p.StartNumber\nreturn response.SendSuccess(c, p, \"ok\")\n```\n\n### `validator`\n\nA helper package for validating structs using [go-playground/validator](https://github.com/go-playground/validator). Supports custom validation rules via `Register`.\n\n**Usage:**\n\n```go\nimport \"github.com/rahmadafandi/fibr/validator\"\n\ntype MyStruct struct {\n    Name string `json:\"name\" validate:\"required\"`\n}\n\nvar body MyStruct\n\nif errs := validator.ValidateStruct(\u0026body); len(errs) \u003e 0 {\n    // Handle validation errors\n}\n\n// Register a custom rule (call once at startup, before concurrent use)\nvalidator.Register(\"my_rule\", func(fl validator.FieldLevel) bool {\n    return fl.Field().String() != \"forbidden\"\n})\n```\n\n### Parse + validate with `bind`\n\n`bind.Body[T]`, `bind.Query[T]`, and `bind.Params[T]` decode a request into `T`\nand run `validator.ValidateStruct` in one call. On malformed input they write a\n`400`; on a validation failure a `422` with per-field errors; otherwise they\nreturn `(value, true)`.\n\n```go\ntype CreateInput struct {\n    Email string `json:\"email\" validate:\"required,email\"`\n}\n\nfunc create(c *fiber.Ctx) error {\n    in, ok := bind.Body[CreateInput](c)\n    if !ok {\n        return nil // bind wrote a 400 (malformed) or 422 (validation) response\n    }\n    return response.SendSuccess(c, in, \"ok\")\n}\n```\n\n### `jwt`\n\nA helper package for working with JSON Web Tokens.\n\n**Usage:**\n\n```go\nimport \"github.com/rahmadafandi/fibr/jwt\"\n\n// Generate a token\ntoken, err := jwt.GenerateToken(claims, secret)\n\n// Validate a token\nvalid, err := jwt.ValidateToken(token, secret)\n```\n\n### `http`\n\nA small JSON HTTP client built on [fasthttp](https://github.com/valyala/fasthttp). All request methods accept a `context.Context` and return `(statusCode int, err error)`.\n\n**Usage:**\n\n```go\nimport (\n    \"context\"\n    \"time\"\n    fhttp \"github.com/rahmadafandi/fibr/http\"\n)\n\nh := fhttp.New(\"https://api.example.com\",\n    fhttp.WithTimeout(10*time.Second),\n    fhttp.WithRetry(3, 500*time.Millisecond),\n    fhttp.WithCircuitBreaker(5, 30*time.Second), // open after 5 consecutive failures\n    fhttp.WithHeader(\"Authorization\", \"Bearer \"+token),\n)\n\nvar result MyResponse\ncode, err := h.Get(ctx, \"/resource/1\", \u0026result)\n\ncode, err = h.Post(ctx, \"/resource\", requestBody, \u0026result)\n\n// Fire and forget (background, non-blocking)\nh.FireAndForget(ctx, fhttp.Post, \"/events\", eventPayload)\n```\n\n`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.\n\n### `retry`\n\nA 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.\n\n```go\nimport \"github.com/rahmadafandi/fibr/retry\"\n\nerr := retry.Do(ctx, func() error {\n    return publish(ctx, msg)\n}, retry.WithAttempts(5), retry.WithDelay(100*time.Millisecond), retry.WithJitter(0.2))\n\n// With a return value, and only retrying some errors:\nuser, err := retry.DoValue(ctx, func() (User, error) {\n    return fetchUser(ctx, id)\n}, retry.WithRetryIf(func(e error) bool { return errors.Is(e, ErrTemporary) }))\n```\n\nBackoff is `delay × multiplier^attempt` (capped by `WithMaxDelay`), jittered by `WithJitter`. The sleep is context-aware, so a cancelled context stops the retries promptly.\n\n### `redis`\n\nA 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.\n\n**Usage:**\n\n```go\nimport (\n    \"context\"\n    \"time\"\n    \"github.com/redis/go-redis/v9\"\n    firedis \"github.com/rahmadafandi/fibr/redis\"\n)\n\nopt, err := firedis.ParseRedisOptions(\"redis://localhost:6379/0\")\nif err != nil {\n    // handle invalid URL\n}\nrds := firedis.New(redis.NewClient(opt))\n\n// Set / Get\n_ = rds.Set(ctx, \"key\", myValue, time.Minute)\n_ = rds.Get(ctx, \"key\", \u0026myValue)\n\n// Cache-aside: returns cached value or calls loader on miss\nresult, err := firedis.Remember(ctx, rds, \"key\", time.Minute, func() (MyType, error) {\n    return expensiveLoad()\n})\n```\n\nInvalidate and inspect entries with `Delete(ctx, keys...)`, `Exists(ctx, key)`,\n`Expire(ctx, key, ttl)`, and `TTL(ctx, key)`.\n\n### Redis-backed rate limiting\n\n```go\nclient := redis.NewClient(\u0026redis.Options{Addr: \"localhost:6379\"})\napp := bootstrap.New(bootstrap.Options{\n    RateLimit:        100,\n    RateLimitStorage: fibrredis.NewStorage(client), // shared across instances\n})\n```\n\n### `slug`\n\nGenerates a unique, URL-safe slug for a given table using a [Bun](https://bun.uptrace.dev/) database.\n\n**Usage:**\n\n```go\nimport (\n    \"context\"\n    \"github.com/rahmadafandi/fibr/slug\"\n)\n\n// Returns e.g. \"my-first-post-abc123defgh456\"\ns, err := slug.Generate(ctx, db, \"posts\", \"My First Post\")\n```\n\n### `uploader`\n\nA 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.\n\n**Usage:**\n\n```go\nimport \"github.com/rahmadafandi/fibr/uploader\"\n\n// Create a local uploader (max 5 MB, images only)\nup := uploader.NewLocalUploader(\"./uploads\",\n    uploader.WithMaxSize(5\u003c\u003c20),\n    uploader.WithAllowedMime([]string{\"image/jpeg\", \"image/png\"}),\n)\n\n// Upload a file (filename is sanitized before saving)\npath, err := up.Upload(file, filename)\n```\n\n### S3-compatible uploads with `uploader.S3Uploader`\n\n```go\nclient, _ := minio.New(endpoint, \u0026minio.Options{Creds: creds, Secure: true})\nup := uploader.NewS3Uploader(client, \"my-bucket\",\n    uploader.WithKeyPrefix(\"avatars/\"),\n    uploader.WithBaseURL(\"https://cdn.example.com/\"),\n)\nurl, err := up.Upload(file, \"photo.png\") // -\u003e https://cdn.example.com/avatars/photo.png\n```\n\n### `middleware`\n\nA collection of useful middleware.\n\n**Usage:**\n\n```go\nimport \"github.com/rahmadafandi/fibr/middleware\"\n\napp := fiber.New()\n\n// Recover from panics\napp.Use(middleware.Recover(logger))\n\n// Log requests\napp.Use(middleware.RequestLogger(logger))\n\n// Protect routes (JWT bearer + revocation/scopes)\napp.Use(auth.RequireAuth(secret))\n\n// Context\napp.Use(middleware.ContextMiddleware(10 * time.Second))\n```\n\n### `context`\n\nAccessors for values stored on the Fiber context: the request-scoped\n`context.Context`, the request ID, and type-safe locals.\n\n**Usage:**\n\n```go\nimport fhctx \"github.com/rahmadafandi/fibr/context\"\n\nctx := fhctx.GetContext(c)        // request-scoped context.Context\nid := fhctx.GetRequestID(c)       // request id (set by ContextMiddleware)\n\nfhctx.SetLocal(c, \"user\", user)   // store a typed value\nu := fhctx.GetLocal[User](c, \"user\") // retrieve it (zero value if absent)\n```\n\n### `database`\n\nOpens a [Bun](https://bun.uptrace.dev/) database, picking the dialect from the DSN\n(`postgres://` → Postgres, `file:`/`:memory:`/path → SQLite).\n\n```go\nimport \"github.com/rahmadafandi/fibr/database\"\n\ndb, err := database.NewBun(\"postgres://localhost/app\",\n    database.WithMaxOpenConns(20),\n    database.WithPingTimeout(3*time.Second),\n)\n```\n\n### `dbresolver`\n\nAn 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.\n\n```go\nimport \"github.com/rahmadafandi/fibr/dbresolver\"\n\nprimary, _ := database.NewBun(\"postgres://primary/app\")\nr1, _ := database.NewBun(\"postgres://replica1/app\")\nr2, _ := database.NewBun(\"postgres://replica2/app\")\n\ndbr := dbresolver.New(primary, r1, r2)\ndefer dbr.Close()\n\n// Writes + read-after-write:\n_, _ = dbr.Writer().NewInsert().Model(\u0026user).Exec(ctx)\n// Reads (round-robin across replicas):\nerr := dbr.Reader().NewSelect().Model(\u0026users).Scan(ctx)\n```\n\nWith no replicas, `Reader()` returns the primary, so the same code works in single-DB setups.\n\n### `migrate`\n\nVersioned database migrations over `bun/migrate`. Declare a collection, register\nGo migrations, and run them from your app binary.\n\n```go\n// internal/migrations/migrations.go\nvar Migrations = migrate.NewMigrations(migrate.WithMigrationsDirectory(\"internal/migrations\"))\n\n// cmd/api/main.go\nroot.AddCommand(migrate.NewCommand(openDB, migrations.Migrations))\n```\n\n`migrate.NewCommand` gives `up`, `down`, `status`, and `create \u003cname\u003e`\nsubcommands. The core funcs `Up`/`Down`/`Status`/`Create` are also usable\ndirectly. Each migration file registers itself in `init()` via\n`Migrations.MustRegister(up, down)`; the version comes from the filename\n(`\u003ctimestamp\u003e_\u003cname\u003e.go`).\n\n### `auth`\n\nJWT bearer authentication and bcrypt password hashing for Fiber.\n\n```go\nhash, _ := auth.Hash(password)          // bcrypt\nerr := auth.Compare(hash, password)     // nil = match\n\napp.Get(\"/me\", auth.RequireAuth(secret), handler)            // 401 if no/invalid token\napp.Get(\"/admin\", auth.RequireAuth(secret),\n    auth.RequireScope(\"admin\"), handler)                     // 403 if scope missing\n\nclaims, ok := auth.Claims(c)            // jwt.MapClaims stored by the middleware\nsub := auth.Subject(c)                  // claims[\"sub\"]\nscopes := auth.Scopes(c)                // normalized []string\n```\n\n`Optional(secret)` validates a token when present but never rejects. Scopes are a\n`scopes` JWT claim (`[]string`). Tokens are minted with the `jwt` package\n(`jwt.GenerateTokenWithExpiry`).\n\n### `health`\n\nLiveness (`/livez`) and readiness (`/readyz`) endpoints with concurrent checks.\n\n```go\nimport \"github.com/rahmadafandi/fibr/health\"\n\ngate := health.NewReadinessGate() // fails /readyz once closed (during drain)\nhealth.Register(app,\n    health.PingBun(db),\n    health.PingRedis(redisClient),\n    health.PingHTTP(\"payments\", \"https://payments.internal/healthz\"),\n    gate.Check(),\n)\n// GET /livez  -\u003e 200 {\"status\":\"ok\"}\n// GET /readyz -\u003e 200/503 {\"status\":\"...\",\"checks\":{...}}\n```\n\nBuilt-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:\n\n```go\nserver.RunGracefulWithConfig(app, \":3000\", server.Config{\n    Timeout:     15 * time.Second,\n    DrainDelay:  5 * time.Second,\n    PreShutdown: []func(context.Context) error{func(context.Context) error { gate.Close(); return nil }},\n    Cleanup:     []func(context.Context) error{func(context.Context) error { return db.Close() }},\n})\n```\n\n### `metrics`\n\nPrometheus request metrics. Standalone:\n\n```go\nimport \"github.com/rahmadafandi/fibr/metrics\"\n\napp.Use(metrics.Middleware())\napp.Get(\"/metrics\", metrics.Handler())\n```\n\nRecords `http_requests_total{method,path,status}` and\n`http_request_duration_seconds{...}`. The `path` label is the Fiber route\ntemplate (e.g. `/items/:id`), so cardinality stays bounded. The default registry\nalso exposes Go-runtime and process collectors (`go_goroutines`, GC, memory,\nfds). The middleware skips its own `/metrics` path.\n\nVia `bootstrap`, enable with `Options{Metrics: true}` — it installs the\nmiddleware and registers `/metrics` ahead of the rate limiter. In a generated\napp, set `METRICS_ENABLED=true`.\n\n### tracing\n\nOpenTelemetry distributed tracing. Set up the provider once at startup and defer\nits shutdown:\n\n```go\nimport \"github.com/rahmadafandi/fibr/tracing\"\n\nshutdown, err := tracing.Setup(ctx, tracing.WithServiceName(\"my-svc\"))\nif err != nil { /* handle */ }\ndefer shutdown(context.Background())\n```\n\n`Setup` builds an OTLP/HTTP exporter (configured by the standard `OTEL_` env vars\nlike `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_SERVICE_NAME`) and installs the global\ntracer provider + W3C propagator. Via `bootstrap`, enable with\n`Options{Tracing: true}` (installs the `otelfiber` server-span middleware) and\npass `shutdown` through `Options{Cleanup: []func(context.Context) error{shutdown}}`\nfor graceful shutdown. When tracing is active, `RequestLogger` adds `trace_id` /\n`span_id` to request logs. In a generated app, set `TRACING_ENABLED=true`.\n\nWhen tracing is enabled, generated apps also install Bun's `bunotel` query hook\n(`database.WithTracing()`), so each SQL query becomes a span nested under the\nrequest span.\n\n### jobs\n\nRedis-backed background jobs built on [asynq](https://github.com/hibiken/asynq).\n\n```go\nimport \"github.com/rahmadafandi/fibr/jobs\"\n\nopt, _ := jobs.RedisConnOpt(os.Getenv(\"REDIS_URL\"))\n\n// enqueue (HTTP side)\nclient := jobs.NewClient(opt)\nclient.Enqueue(ctx, \"welcome:send\", WelcomePayload{Email: \"a@b.com\"})\n\n// process (worker side)\nsrv := jobs.NewServer(opt, jobs.ServerConfig{Concurrency: 10})\njobs.Handle[WelcomePayload](srv, \"welcome:send\", handleWelcome)\nsrv.Run()\n```\n\n`Enqueue` JSON-marshals the payload; the generic `Handle[T]` decodes it back into\n`T` before calling your handler (a malformed payload wraps `asynq.SkipRetry` so it\nis not retried forever). Mount the\n[asynqmon](https://github.com/hibiken/asynqmon) dashboard through `bootstrap`\n(the asynqmon dependency stays out of `bootstrap` itself):\n\n```go\nbootstrap.New(bootstrap.Options{\n    Asynqmon: \u0026bootstrap.AsynqmonMount{\n        Handler: jobs.MonitoringHandler(opt, \"/monitoring\"),\n        Path:    \"/monitoring\",\n    },\n})\n```\n\nGenerate an app with the queue scaffolded via `fibr new --queue`: it adds\na `worker` subcommand, a sample job, the monitoring UI mount, and the\n`REDIS_URL` / `QUEUE_CONCURRENCY` / `ASYNQMON_PATH` config keys. When `REDIS_URL`\nis unset the queue is disabled with a startup warning (the `worker` subcommand\nexits with an error).\n\n### Scheduled jobs with `jobs.Scheduler`\n\n```go\nsched := jobs.NewScheduler(opt) // opt from jobs.RedisConnOpt(redisURL)\nif _, err := sched.Register(\"0 2 * * *\", \"cleanup:run\", CleanupPayload{OlderThanDays: 30}); err != nil {\n    log.Fatal(err)\n}\nlog.Fatal(sched.Run()) // run ONE instance; workers process the enqueued tasks\n```\n\n### `lock`\n\nA 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.\n\n```go\nlocker := lock.New(redisClient) // redisClient is a redis.UniversalClient\n\n// Run-once across replicas: acquires, runs fn, releases. Returns\n// lock.ErrNotAcquired (without running fn) if another replica holds it.\nerr := locker.Do(ctx, \"cron:nightly-cleanup\", 30*time.Second, func() error {\n    return cleanup(ctx)\n})\nif err != nil \u0026\u0026 !errors.Is(err, lock.ErrNotAcquired) {\n    log.Fatal(err)\n}\n```\n\nFor 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.\n\n### `outbox`\n\nA 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).\n\n```go\n// Once, at startup:\n_ = outbox.Migrate(ctx, db)\n\n// In your handler/service, within the business transaction:\nerr := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {\n    if _, err := tx.NewInsert().Model(\u0026order).Exec(ctx); err != nil {\n        return err\n    }\n    return outbox.Enqueue(ctx, tx, \"order.created\", OrderCreated{ID: order.ID})\n})\n\n// In a background worker (run one logical relay; pass a lock.Locker to make it\n// safe to run on every replica — only one relays at a time):\nrelay := outbox.NewRelay(db, outbox.NewRedisPublisher(redisClient),\n    outbox.WithLock(lock.New(redisClient), \"outbox:relay\", 30*time.Second))\ngo relay.Run(ctx)\n```\n\nConsumers subscribe with `redis.Subscribe[OrderCreated](...)` — the relay publishes the stored JSON bytes verbatim, so they decode directly. Published rows are kept (for audit); purge them with `DELETE FROM outbox WHERE published_at \u003c ?` on your own schedule.\n\n### `inbox`\n\nThe 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.\n\n```go\nimport \"github.com/rahmadafandi/fibr/inbox\"\n\n_ = inbox.Migrate(ctx, db) // once at startup\n\n// In your message/event handler:\nerr := inbox.Once(ctx, db, msg.ID, func(ctx context.Context, tx bun.Tx) error {\n    // Do the work through tx so it commits atomically with the dedup marker.\n    _, err := tx.NewInsert().Model(\u0026order).Exec(ctx)\n    return err\n})\n```\n\nPair it with `outbox` (producer) for end-to-end exactly-once-effect processing over an at-least-once transport. Old markers can be purged with `DELETE FROM inbox WHERE processed_at \u003c ?`.\n\n### `events`\n\nAn 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.\n\n```go\nimport \"github.com/rahmadafandi/fibr/events\"\n\ntype OrderCreated struct{ ID int }\n\nbus := events.New() // or events.New(events.WithAsync())\nevents.Subscribe(bus, func(ctx context.Context, e OrderCreated) error {\n    return mailer.SendOrderConfirmation(ctx, e.ID)\n})\n\n// Producer — runs handlers now, returns their joined errors (sync mode).\nif err := events.Publish(ctx, bus, OrderCreated{ID: order.ID}); err != nil {\n    log.Error(err, \"order.created handlers failed\")\n}\n```\n\n### `featureflag`\n\nRuntime 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.\n\n```go\nimport \"github.com/rahmadafandi/fibr/featureflag\"\n\nflags := featureflag.New(featureflag.Rules(map[string]featureflag.Rule{\n    \"new_checkout\": {Percentage: 25, Users: []string{\"vip\"}},\n}))\n\n// Direct evaluation:\nif flags.Enabled(ctx, \"new_checkout\", featureflag.Eval{UserID: uid}) { ... }\n\n// Or via Fiber: derive the eval context from the request, then check per-handler.\napp.Use(flags.Middleware(func(c *fiber.Ctx) featureflag.Eval {\n    return featureflag.Eval{UserID: auth.UserID(c)}\n}))\napp.Get(\"/checkout\", func(c *fiber.Ctx) error {\n    if featureflag.Enabled(c, \"new_checkout\") {\n        return newCheckout(c)\n    }\n    return oldCheckout(c)\n})\n```\n\nWith 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.\n\n### `audit`\n\nAn 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.\n\n```go\nimport \"github.com/rahmadafandi/fibr/audit\"\n\n_ = audit.Migrate(ctx, db) // once at startup\n\nrec := audit.New(audit.NewBunSink(db), audit.WithActor(func(c *fiber.Ctx) string {\n    return auth.Subject(c) // who is acting\n}))\n\napp.Delete(\"/orders/:id\", func(c *fiber.Ctx) error {\n    if err := orders.Delete(c.UserContext(), c.Params(\"id\")); err != nil {\n        return err\n    }\n    e := rec.FromRequest(c) // prefills actor, IP, request id\n    e.Action, e.Target, e.TargetID = \"order.delete\", \"order\", c.Params(\"id\")\n    _ = rec.Record(c.UserContext(), e)\n    return c.SendStatus(fiber.StatusNoContent)\n})\n\n// Read back:\nentries, _ := audit.List(ctx, db, audit.Filter{Actor: \"u1\", Limit: 50})\n```\n\n`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.\n\n### `cache`\n\nA 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; `redis.Remember` remains the choice for a cache shared across instances.\n\n```go\nimport \"github.com/rahmadafandi/fibr/cache\"\n\nc := cache.New[User](\n    cache.WithMaxSize(10_000),\n    cache.WithDefaultTTL(time.Minute),\n    cache.WithJanitor(time.Minute), // optional background sweep; call c.Close() to stop\n)\n\n// Load-through: runs the loader once on a miss even under concurrent callers.\nu, err := c.GetOrLoad(ctx, \"user:1\", func() (User, error) {\n    return repo.FindUser(ctx, 1)\n})\n\nc.Set(\"k\", v); v, ok := c.Get(\"k\"); c.Delete(\"k\")\n```\n\n`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.\n\n### `ratelimit`\n\nA 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.\n\n```go\nimport \"github.com/rahmadafandi/fibr/ratelimit\"\n\nl := ratelimit.New(redisClient)\nrule := ratelimit.Rule{Capacity: 100, RefillPerSec: 10} // burst 100, sustain 10/s\n\n// Direct:\nres, err := l.Allow(ctx, \"user:\"+uid, rule, 1)\nif !res.Allowed { /* res.RetryAfter, res.Remaining */ }\n\n// Fiber middleware — 429 + Retry-After + X-RateLimit-* on deny:\napp.Use(l.Middleware(rule, func(c *fiber.Ctx) string { return c.IP() }))\n```\n\nA 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.\n\n### `apikey`\n\nAPI-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.\n\n```go\nimport \"github.com/rahmadafandi/fibr/apikey\"\n\n// Issue a key: hand `key` to the client, persist `hash`.\nkey, hash, _ := apikey.Generate()\n\na := apikey.New(apikey.Config{Store: apikey.MapStore(map[string]apikey.Identity{\n    hash: {ID: \"service-a\", Scopes: []string{\"read\"}},\n})})\n\napp.Use(a.Middleware())                       // reads X-API-Key → 401 or sets identity\napp.Get(\"/data\", a.RequireScope(\"read\"), handler)\n\napp.Get(\"/whoami\", func(c *fiber.Ctx) error {\n    id, _ := apikey.FromContext(c)\n    return c.JSON(id) // {ID, Scopes, Meta}\n})\n```\n\n`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.\n\n### mailer\n\nTransactional email through a pluggable `Sender`.\n\n```go\nimport \"github.com/rahmadafandi/fibr/mailer\"\n\nsender, _ := mailer.New(mailer.SMTPConfig{\n    Host: os.Getenv(\"SMTP_HOST\"), Port: 587,\n    Username: os.Getenv(\"SMTP_USERNAME\"), Password: os.Getenv(\"SMTP_PASSWORD\"),\n    From: \"no-reply@example.com\",\n})\n\nhtml, text, _ := mailer.Render(\"\u003cp\u003eHi {{.Name}}\u003c/p\u003e\", \"Hi {{.Name}}\", data)\nsender.Send(ctx, mailer.Message{To: []string{\"a@b.com\"}, Subject: \"Hi\", HTML: html, Text: text})\n```\n\n`New` returns an SMTP sender when `Host` is set, otherwise a `LogSender` that\nlogs instead of sending (handy in development). A `MemorySender` captures\nmessages for tests. Because `Message` is JSON-serializable it doubles as an\nasynq job payload — generated apps with both `--mailer` and `--queue` send\nasynchronously through an `email:send` job; with `--mailer` alone they send\ninline. `fibr new --mailer` also sends the team invitation email (with\n`--auth-with-team`) and the welcome job (with `--queue`).\n\n### `server`\n\nSignal-based graceful shutdown.\n\n```go\nimport \"github.com/rahmadafandi/fibr/server\"\n\nerr := server.RunGraceful(app, \":3000\", 10*time.Second, func(ctx context.Context) error {\n    return db.Close()\n})\n```\n\n### Typed errors with `apierror`\n\n`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.\n\n```go\nfunc getUser(c *fiber.Ctx) error {\n    u, err := svc.Find(c.UserContext(), id)\n    if err != nil {\n        return apierror.NotFound(\"user not found\").WithCode(\"user_not_found\")\n    }\n    return response.SendSuccess(c, u, \"ok\")\n}\n// bootstrap.New installs apierror.Handler, so the return renders as:\n// {\"code\":404,\"message\":\"user not found\",\"error\":\"user_not_found\",\"status\":\"error\"}\n```\n\n### `bootstrap`\n\nOptional one-call wiring of recover, request id, logging, optional CORS / rate\nlimit / health, and graceful shutdown.\n\n```go\nimport \"github.com/rahmadafandi/fibr/bootstrap\"\n\napp := bootstrap.New(bootstrap.Options{\n    DB:           db,\n    EnableCORS:   true,\n    RateLimit:    100,\n    HealthChecks: []health.NamedCheck{health.PingBun(db)},\n})\napp.Get(\"/\", handler)\nlog.Fatal(app.Run(\":3000\")) // graceful shutdown + db.Close handled\n```\n\n### `fibrtest`\n\nA 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.\n\n```go\nimport \"github.com/rahmadafandi/fibr/fibrtest\"\n\nfunc TestCreateThing(t *testing.T) {\n    app := bootstrap.New(bootstrap.Options{DB: fibrtest.NewDB(t)})\n    app.Post(\"/things\", createThing)\n\n    c := fibrtest.New(t, app.App).\n        WithBearer(fibrtest.Token(t, secret, jwt.MapClaims{\"sub\": \"u1\"}))\n\n    var out Thing\n    c.Post(\"/things\", ThingReq{Name: \"x\"}).ExpectStatus(201).JSON(\u0026out)\n    assert.Equal(t, \"x\", out.Name)\n}\n```\n\n`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.\n\n### Modules\n\nA `Module` is a self-contained feature that registers its own routes. Mount it\nin one line; it can optionally migrate its tables and report health.\n\n```go\ntype Module interface {\n    Name() string\n    Register(r fiber.Router) error\n}\n// optional, detected via type assertion:\ntype Migrator      interface{ Migrate(ctx context.Context) error }\ntype HealthChecker interface{ Checks() []health.NamedCheck }\n\napp := bootstrap.New(bootstrap.Options{DB: db})\nif err := app.Mount(user.NewUserModule(db), product.NewProductModule(db)); err != nil {\n    log.Fatal(err)\n}\n```\n\nBy default `Mount` does **not** create tables — use migrations\n(`migrate.NewCommand`). Set `bootstrap.Options{AutoMigrate: true}` (e.g. from an\n`AUTO_MIGRATE` env in dev) to have `Mount` run each module's `Migrate` at startup.\n\n`Mount` registers each module's routes and, when `AutoMigrate` is enabled, runs\nits `Migrate`. It also adds its `Checks()` to `/readyz`.\n\n\u003e Note: `*bootstrap.App.Mount` is the module-aware method and shadows Fiber's\n\u003e `Mount`. Where a `fiber.Router` is needed (e.g. passing the app to a route\n\u003e registrar), use the embedded `app.App`.\n\n## License\n\n[MIT](LICENSE) © 2026 Rahmad Afandi\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frahmadafandi%2Ffibr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frahmadafandi%2Ffibr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frahmadafandi%2Ffibr/lists"}