
fiberfx
Uber Fx module for Fiber v3 — HTTP server with structured logging, health probes, Prometheus metrics, OpenAPI docs and request validation.
Report Bug
·
Request Feature
Table of Contents
About The Project
fiberfx is an Uber Fx module that builds and runs a Fiber v3 HTTP server. It wires the server into the Fx lifecycle — the listener starts on OnStart and shuts down gracefully on OnStop — and ships the cross-cutting pieces most services end up writing by hand:
- Lifecycle-managed server —
Module() provides *fiber.App, opens the listener on the Fx start hook and drains connections on shutdown
- Structured request logging — request ID, latency, status, method, URL, IP, user agent and error fields, rendered through
zap
- JSON error contract — one
ErrorResponse shape for every error the server returns
- Health probes — liveness, readiness and startup endpoints backed by
healthfx
- Prometheus metrics — opt-in
/metrics endpoint, on an injected registry or the process-wide default
- OpenAPI documentation — Swagger UI and
doc.json served from the generated spec
- Request validation — struct-tag validation with per-field error details and custom
Validate() hooks
This is a library, not an application: it reads no environment variables and has no configuration file. Everything is configured through Go structs (fiberfx.Config, fiberfx.Options and the per-feature config types), which the host application supplies to the Fx graph.
Features
- Fiber v3 — built on
github.com/gofiber/fiber/v3
- Uber Fx module —
fiberfx.Module() provides *fiber.App and manages the listener lifecycle
- Graceful shutdown —
app.ShutdownWithContext(ctx) on the Fx stop hook; a listener failure triggers fx.Shutdown
- Request ID propagation — the
X-Request-ID response header is set per request and logged
- Panic recovery — the Fiber
recover middleware is installed by default
- IP validation — enabled by default, so
c.IP() returns a single validated address instead of a raw X-Forwarded-For chain
- Always-unescaped paths — request paths are unescaped before routing
- Proxy trust — optional trusted-proxy list and proxy header
- Full Fiber configuration — any
fiber.Config field (error handler, template engine, body limit, GET-only mode) can be overridden through a callback
- Configurable log fields — the request log field set can be replaced
- Health, metrics and OpenAPI — the three operational endpoints every service needs
Built With
(back to top)
Getting Started
Prerequisites
Installation
go get github.com/go-core-fx/fiberfx@latest
(back to top)
Usage
Module Setup
fiberfx.Module() provides *fiber.App. The graph must also contain fiberfx.Config, fiberfx.Options and a base *zap.Logger; a *promclient.Registry is optional and only needed for a custom metrics registry.
package main
import (
"github.com/go-core-fx/fiberfx"
"github.com/gofiber/fiber/v3"
"go.uber.org/fx"
"go.uber.org/zap"
)
func main() {
fx.New(
// Base logger. The module decorates it with the name "http".
fx.Provide(zap.NewProduction),
fx.Provide(func() fiberfx.Config {
return fiberfx.Config{
Address: ":8080",
}
}),
fx.Provide(func(logger *zap.Logger) fiberfx.Options {
var opts fiberfx.Options
opts.WithFiberConfig(func(cfg *fiber.Config) {
cfg.ErrorHandler = fiberfx.NewJSONErrorHandler(logger)
})
opts.WithMetrics()
return opts
}),
fiberfx.Module(),
).Run()
}
Options is configured through pointer receivers, so build it as a local variable and supply the value:
opts := fiberfx.Options{}
opts.WithMetrics()
fx.Supply(opts)
The module starts the listener on the Fx start hook and shuts it down on the stop hook, logging
starting server, server listening, shutting down server and server shutdown completed through the
http logger.
The *fiber.App is available for route registration, either by injecting it or by populating it from the graph:
var app *fiber.App
fx.New(
// ...
fiberfx.Module(),
fx.Populate(&app),
).Run()
Configuration Reference
| Field |
Type |
Default |
Description |
Address |
string |
— |
Listen address passed to net.Listen (for example ":8080" or "127.0.0.1:8080"). No default. |
ProxyHeader |
string |
"" |
Request header holding the client IP. Only used when Proxies is non-empty. |
Proxies |
[]string |
nil |
Trusted proxy IPs or CIDR ranges. When empty, proxy headers are ignored entirely. |
Proxy trust is enabled only when Proxies is non-empty — with an empty list the client IP is taken from the
remote address and ProxyHeader has no effect.
Options
| Option |
Effect |
WithFiberConfig(configure) |
Applies changes to the Fiber configuration after the package defaults — see Fiber Configuration. |
WithRequestLogFields(fields...) |
Replaces the fields written to the request log. An empty list keeps the defaults — see Request Logging. |
WithMetrics() |
Mounts the Prometheus metrics endpoint at /metrics. |
Fiber Configuration
WithFiberConfig hands the assembled fiber.Config to a callback, so anything Fiber supports can be
overridden — the error handler, the template engine, the body limit, the read/write timeouts and so on:
opts := fiberfx.Options{}
opts.WithFiberConfig(func(cfg *fiber.Config) {
cfg.ErrorHandler = fiberfx.NewJSONErrorHandler(logger)
cfg.Views = views
cfg.BodyLimit = 30 * 1024 * 1024
cfg.GETOnly = true
})
The callback runs after the package defaults are applied (EnableIPValidation, the trusted proxies from
fiberfx.Config and UnescapePath), so it can override them.
Request Logging
Every request is logged with requestId, latency, status, method, url, ip, ua, body and error.
WithRequestLogFields replaces that list; an empty list keeps the defaults:
opts := fiberfx.Options{}
opts.WithRequestLogFields("requestId", "status", "error")
Health and metrics paths are never logged.
Error Handling
All errors the server returns use one shape:
{
"message": "Not Found",
"code": 404
}
message and code are always present; details is optional and carries structured context when there is any.
Three handlers are provided, and are installed through WithFiberConfig:
// JSON error response — the default choice for APIs.
fiberfx.NewJSONErrorHandler(logger)
// JSON error response with a custom payload.
fiberfx.NewCustomJSONErrorHandler(logger, func(err error, code int) any {
return fiberfx.NewErrorResponse("something went wrong", code, nil)
})
// HTML error page rendered through a fiber.Views engine.
fiberfx.NewViewsErrorHandler(logger, "error", "layouts/main")
Status codes are derived from the error:
| Source |
Status |
*fiber.Error |
the error's own code |
context.Canceled |
499 (fiberfx.StatusClientClosedRequest) |
context.DeadlineExceeded |
408 |
| anything else |
500 |
For a 4xx the raw error message is returned, except for 408 and 499 which get a fixed text; for a 5xx
the standard status text is returned instead, so internal details never leak to clients. Errors with a 5xx status
are logged through the logger that was passed to the handler.
499 is non-standard but widely used. If your infrastructure only accepts standard codes, map
fiberfx.StatusClientClosedRequest to fiber.StatusRequestTimeout in a custom handler.
Health Probes
The health package exposes liveness, readiness and startup probes backed by
healthfx:
app := fiberfx.New(cfg, opts, registry, logger)
// /health, /health/live, /health/ready, /health/startup
health.NewHandler(healthSvc).Register(app)
| Route |
Meaning |
/health |
alias of /health/live |
/health/live |
the process is running |
/health/ready |
the process can serve traffic |
/health/startup |
the process finished initializing |
A failed probe responds 503, a passing one 200:
{
"status": "pass",
"version": "1.0.0",
"releaseId": 42,
"checks": {
"system:goroutines": {
"description": "Number of goroutines",
"observedUnit": "goroutines",
"observedValue": 12,
"status": "pass"
}
}
}
version and releaseId come from the healthfx.Version held by the health service. Check names are prefixed
with the provider name. The overall status is pass, warn or fail; only fail maps to 503.
Prometheus Metrics
WithMetrics() mounts the Fiber Prometheus middleware at /metrics:
opts := fiberfx.Options{}
opts.WithMetrics()
Metrics land in the process-wide default registry unless a *promclient.Registry is present in the graph:
fx.Provide(func() *promclient.Registry {
return promclient.NewRegistry()
})
An injected registry starts empty, so the middleware registers the Go and process collectors into it itself; the
default registry already contains them. The exposed families are http_requests_total,
http_requests_status_class_total, http_request_duration_seconds, http_request_size_bytes,
http_response_size_bytes and http_requests_in_progress.
OpenAPI Documentation
The openapi package serves a Swagger UI and the generated spec from a swag.Spec:
spec := &swag.Spec{
SwaggerTemplate: `{
"swagger": "2.0",
"info": {"title": "My API", "version": "{{.Version}}"},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {}
}`,
}
swag.Register(swag.Name, spec)
openapi.NewHandler(
openapi.Config{
Enabled: true,
PublicHost: "api.example.com",
PublicPath: "/v1",
},
healthfx.Version{Version: "1.0.0", ReleaseID: 1},
spec,
).Register(app.Group("/swagger"))
| Field |
Default |
Description |
Enabled |
false |
When false the handler registers no routes at all. |
PublicHost |
"" |
Host written into the spec. When empty, the host of the first request is used. |
PublicPath |
"" |
Base path written into the spec. When empty, the base path from the spec is kept. |
With the group mounted at /swagger, the UI is served at /swagger/index.html and the spec at
/swagger/doc.json. Responses carry a weak ETag.
Request Validation
The validation package validates request bodies and returns per-field error details:
type CreateUserRequest struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
app.Post("/users",
validation.Middleware,
validation.NewBody[CreateUserRequest](validator.New()),
validation.DecorateWithBody(func(c fiber.Ctx, body *CreateUserRequest) error {
return c.Status(fiber.StatusCreated).JSON(body)
}),
)
validation.Middleware converts a validation failure into a 400 response:
{
"message": "validation failed: name: name is required",
"code": 400,
"details": [
{
"field": "name",
"tag": "required",
"message": "name is required"
}
]
}
| Function |
Purpose |
validation.Middleware |
Middleware that renders validation failures as a 400 JSON response. |
validation.NewBody[T](validate) |
Validates the body and stores the result in the request context. |
validation.ValidateBody[T](c, validate) |
Validates the body and returns it directly. |
validation.GetValidatedBody[T](c) |
Reads the validated body stored by NewBody. |
validation.DecorateWithBody[T](h) |
Wraps a handler so it receives the validated body. |
validation.DecorateWithBodyEx[T](validate, h) |
Validates and wraps in one step, without the middleware. |
A request type may implement Validate() error for rules that struct tags cannot express. It runs after the
tag validation and its error is reported the same way:
func (r *CreateUserRequest) Validate() error {
if r.Name == "admin" {
return errors.New("name is reserved")
}
return nil
}
Handler Base Helpers
handler.Base binds and validates a request in one call. It is useful when a route needs the parsed values
directly instead of a validated body in the context:
base := &handler.Base{Validator: validator.New()}
app.Get("/users/:id", func(c fiber.Ctx) error {
var out struct {
ID string `uri:"id" validate:"required"`
}
if err := base.ParamParserValidator(c, &out); err != nil {
return err
}
return c.SendString(out.ID)
})
| Method |
Binds from |
Struct tag (Fiber v3) |
Fiber v2 tag |
BodyParserValidator(c, out) |
request body |
json |
json |
QueryParserValidator(c, out) |
query string |
query |
query |
ParamParserValidator(c, out) |
route parameters |
uri |
params |
HeaderParserValidator(c, out) |
request headers |
header |
reqHeader |
The Fiber v3 binder reads uri and header; the v2 tag names are not read.
JSON Content Type
jsonify makes responses advertise application/json when the client accepts JSON and the body is empty or
already valid JSON. Bodies that are not valid JSON are left untouched:
app.Use(jsonify.New())
Status Code Middleware
statuscode answers with a fixed status code and message, for example to return 404 for unmatched routes
instead of Fiber's default response:
app.Use(statuscode.New(statuscode.Config{
StatusCode: fiber.StatusNotFound,
StatusMessage: "Not Found",
}))
Both fields are optional; they default to 404 and Not Found. Next skips the middleware for matching
requests:
app.Use(statuscode.New(statuscode.Config{
Next: func(c fiber.Ctx) bool {
return c.Path() == "/health"
},
}))
(back to top)
Roadmap
- Fiber v3 server wired into the Fx lifecycle
- Graceful shutdown
- Structured request logging with request ID
- Configurable request log fields
- JSON error contract and customizable error handlers
- Health probes (liveness, readiness, startup)
- Prometheus metrics with an optional injected registry
- OpenAPI / Swagger UI
- Request validation with per-field errors
- Trusted proxy support
- Full Fiber configuration override
See the open issues for a full list of proposed features.
(back to top)
Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature)
- Commit your Changes (
git commit -m 'Add some AmazingFeature')
- Push to the Branch (
git push origin feature/AmazingFeature)
- Open a Pull Request
(back to top)
License
Distributed under the Apache License 2.0. See LICENSE for more information.
(back to top)
Acknowledgments
(back to top)