diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 000000000000..c29af874b1e4 --- /dev/null +++ b/.cspell.json @@ -0,0 +1,83 @@ +{ + "version": "0.2", + "language": "en, en-gb, en-us", + "useGitignore": true, + "caseSensitive": false, + "import": [ + "@cspell/dict-en_us/cspell-ext.json", + "@cspell/dict-en-gb/cspell-ext.json", + "@cspell/dict-software-terms/cspell-ext.json", + "@cspell/dict-golang/cspell-ext.json", + "@cspell/dict-fullstack/cspell-ext.json", + "@cspell/dict-docker/cspell-ext.json", + "@cspell/dict-k8s/cspell-ext.json", + "@cspell/dict-node/cspell-ext.json", + "@cspell/dict-npm/cspell-ext.json", + "@cspell/dict-typescript/cspell-ext.json", + "@cspell/dict-html/cspell-ext.json", + "@cspell/dict-css/cspell-ext.json", + "@cspell/dict-shell/cspell-ext.json", + "@cspell/dict-python/cspell-ext.json", + "@cspell/dict-redis/cspell-ext.json", + "@cspell/dict-sql/cspell-ext.json", + "@cspell/dict-filetypes/cspell-ext.json", + "@cspell/dict-companies/cspell-ext.json", + "@cspell/dict-markdown/cspell-ext.json", + "@cspell/dict-en-common-misspellings/cspell-ext.json", + "@cspell/dict-people-names/cspell-ext.json" + ], + "dictionaries": [ + "en_us", + "en-gb", + "softwareTerms", + "web-services", + "networking-terms", + "software-term-suggestions", + "software-services", + "software-terms", + "software-tools", + "coding-compound-terms", + "golang", + "fullstack", + "docker", + "k8s", + "node", + "npm", + "typescript", + "html", + "css", + "shell", + "python", + "redis", + "sql", + "filetypes", + "companies", + "markdown", + "en-common-misspellings", + "people-names", + "data-science", + "data-science-models", + "data-science-tools", + "project-words" + ], + "dictionaryDefinitions": [ + { + "name": "project-words", + "path": "./project-words.txt", + "addWords": true + } + ], + "files": [ + "blog/**", + "src/**" + ], + "ignorePaths": [ + "**/*.svg", + "**/*.png", + "**/*.jpg", + "**/*.jpeg", + "**/*.gif", + "**/*.ico", + "**/*.lock" + ] +} diff --git a/.github/workflows/spell-check.yml b/.github/workflows/spell-check.yml new file mode 100644 index 000000000000..ad33ec9a6aa3 --- /dev/null +++ b/.github/workflows/spell-check.yml @@ -0,0 +1,71 @@ +name: Spell check + +on: + workflow_dispatch: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + push: + branches: + - main + - master + +permissions: + contents: read + pull-requests: read + +jobs: + cspell: + name: cspell + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: "20.x" + + - name: Install cspell dictionaries + run: | + npm install --no-save \ + @cspell/dict-en_us \ + @cspell/dict-en-gb \ + @cspell/dict-software-terms \ + @cspell/dict-golang \ + @cspell/dict-fullstack \ + @cspell/dict-docker \ + @cspell/dict-k8s \ + @cspell/dict-node \ + @cspell/dict-npm \ + @cspell/dict-typescript \ + @cspell/dict-html \ + @cspell/dict-css \ + @cspell/dict-shell \ + @cspell/dict-python \ + @cspell/dict-redis \ + @cspell/dict-sql \ + @cspell/dict-filetypes \ + @cspell/dict-companies \ + @cspell/dict-markdown \ + @cspell/dict-en-common-misspellings \ + @cspell/dict-people-names \ + @cspell/dict-data-science + + - name: Run cspell + uses: streetsidesoftware/cspell-action@de2a73e963e7443969755b648a1008f77033c5b2 # v8.4.0 + with: + files: "blog/** src/**" + incremental_files_only: false + check_dot_files: explicit + report: typos + verbose: true + + - name: Run codespell + uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2 + with: + path: blog src + ignore_words_list: TE,te diff --git a/Makefile b/Makefile new file mode 100644 index 000000000000..305480328d6b --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +## help: π‘ Display available commands +.PHONY: help +help: + @echo 'β‘οΈ GoFiber/Docs Development:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +## install: π¦ Install dependencies +.PHONY: install +install: + npm install + +## dev: π Start development server +.PHONY: dev +dev: + npm start + +## build: π Build production site +.PHONY: build +build: + npm run build + +## serve: π Serve production build locally +.PHONY: serve +serve: + npm run serve + +## spell: π Run spell check on blog and website source +.PHONY: spell +spell: + npx cspell "blog/**" "src/**" --no-progress + +## codespell: π Run codespell on blog and website source +.PHONY: codespell +codespell: + codespell blog src --ignore-words-list "TE,te" + +## markdown: π¨ Find markdown format issues (Requires markdownlint-cli2) +.PHONY: markdown +markdown: + npx markdownlint-cli2 "blog/**/*.md" + +## lint: π¨ Run all lint checks (spell + markdown) +.PHONY: lint +lint: spell codespell markdown + +## format: π¨ Fix code format issues +.PHONY: format +format: + npx prettier --write "src/**/*.{ts,tsx,scss,css,json}" "blog/**/*.md" + +## clean: π§Ή Clean build artifacts +.PHONY: clean +clean: + rm -rf build .docusaurus diff --git a/blog/2026-02-11-whats-new-in-fiber-v3.md b/blog/2026-02-11-whats-new-in-fiber-v3.md index 27782a873bbc..560f2b6029af 100644 --- a/blog/2026-02-11-whats-new-in-fiber-v3.md +++ b/blog/2026-02-11-whats-new-in-fiber-v3.md @@ -75,7 +75,7 @@ app.Post("/users/:id", func(c fiber.Ctx) error { v3 binding also supports built-in validation, default values via `default:` tags, and custom binders for formats like YAML. Native CBOR and MsgPack support is included alongside JSON, XML, and form data. -For a deep dive, see [Binding in Practice](/blog/fiber-v3-binding-in-practice). +For a deep dive, see [Binding in Practice](./2026-02-16-fiber-v3-binding-in-practice.md). ## 2) Lifecycle Hooks: Deploy Confidence @@ -103,7 +103,7 @@ app.Hooks().OnPostShutdown(func(err error) error { v3 also adds registration-time hooks (`OnRoute`, `OnGroup`, `OnName`, `OnMount`) that fire when routes and sub-apps are registered, useful for building route registries and enforcing naming conventions. -For a deep dive, see [Hooks Guide for Clean Lifecycles](/blog/fiber-v3-hooks-guide). +For a deep dive, see [Hooks Guide for Clean Lifecycles](./2026-02-20-fiber-v3-hooks-guide.md). ## 3) Listen: Cleaner Configuration @@ -155,7 +155,7 @@ This enables route-by-route migration. You modernize high-value endpoints first v3 also adds `RouteChain` for Express-style route declaration and automatically registers `HEAD` routes for every `GET` route. -For a deep dive, see [Handler Compatibility in the New Router](/blog/fiber-v3-adapter-pattern). +For a deep dive, see [Handler Compatibility in the New Router](./2026-02-19-fiber-v3-adapter-pattern.md). ## 5) Context Implements `context.Context` @@ -174,7 +174,7 @@ Deadline propagation, cancellation, and request-scoped values all work natively. v3 also adds many new context methods: `Drop()` for silent connection termination (DDoS mitigation), `End()` for immediate response flush, `SendEarlyHints()` for HTTP 103 preloading, `AutoFormat()` for content negotiation, `SendStreamWriter()` for SSE and streaming, and numerous inspection helpers like `IsJSON()`, `IsWebSocket()`, `HasBody()`, and `IsPreflight()`. -For a deep dive on context extensions, see [Custom Context in Practice](/blog/fiber-v3-custom-context). +For a deep dive on context extensions, see [Custom Context in Practice](./2026-02-17-fiber-v3-custom-context.md). ## 6) Generic Functions: Type-Safe Parameter Access @@ -208,7 +208,7 @@ app.Use(keyauth.New(keyauth.Config{ `FromAuthHeader` includes strict RFC 9110/7235 validation. The chain tries each source in order and returns the first success, making extraction policy explicit and auditable. -For a deep dive, see [Extractors Guide for Middleware](/blog/fiber-v3-extractors-guide). +For a deep dive, see [Extractors Guide for Middleware](./2026-02-21-fiber-v3-extractors-guide.md). ## 8) Custom Route Constraints @@ -240,13 +240,13 @@ v3 makes several protocol improvements that reduce interoperability incidents: - **Non-ASCII filenames**: `Attachment` and `Download` use RFC 6266/8187 encoding - **Default redirect status**: Changed from 302 to 303 for more consistent browser behavior -For a deep dive, see [RFC Conformance in Practice](/blog/fiber-v3-rfc-conformance). +For a deep dive, see [RFC Conformance in Practice](./2026-02-18-fiber-v3-rfc-conformance.md). ## 10) Storage, Client, and Middleware Updates **Storage**: All storage adapters now include `WithContext` methods (`GetWithContext`, `SetWithContext`, `DeleteWithContext`, `ResetWithContext`) for cancellation, timeout control, and request-scoped behavior. -**Client**: The client package has been completely rebuilt with cookie jar support, request/response hooks, retry configuration, proxy support, and debug mode. See [New Client Deep Dive](/blog/fiber-v3-client-deep-dive). +**Client**: The client package has been completely rebuilt with cookie jar support, request/response hooks, retry configuration, proxy support, and debug mode. See [New Client Deep Dive](./2026-02-15-fiber-v3-client-deep-dive.md). **Middleware changes worth noting**: - `app.Static()` removed β use the [static middleware](/middleware/static) instead diff --git a/blog/2026-02-12-build-a-crud-app-with-fiber.md b/blog/2026-02-12-build-a-crud-app-with-fiber.md index 2ecf581b2f7c..b19e69642750 100644 --- a/blog/2026-02-12-build-a-crud-app-with-fiber.md +++ b/blog/2026-02-12-build-a-crud-app-with-fiber.md @@ -87,7 +87,7 @@ type CreateBook struct { } ``` -See the [Binding in Practice](/blog/fiber-v3-binding-in-practice) post for the full validation setup. +See the [Binding in Practice](./2026-02-16-fiber-v3-binding-in-practice.md) post for the full validation setup. ## What Actually Happens in a CRUD Request @@ -145,7 +145,7 @@ These are not just demo commands. They are a minimum regression checklist for an ## Practical Lessons Before You Ship This Pattern -The recipe updates and deletes by title. That is fine for learning, but in production you should usually move to immutable identifiers (numeric ID, UUID, ULID). Fiber v3 supports [custom route constraints](/blog/whats-new-in-fiber-v3#8-custom-route-constraints) that can validate identifier formats at the routing layer, so invalid IDs never reach your handler. +The recipe updates and deletes by title. That is fine for learning, but in production you should usually move to immutable identifiers (numeric ID, UUID, ULID). Fiber v3 supports [custom route constraints](./2026-02-11-whats-new-in-fiber-v3.md#8-custom-route-constraints) that can validate identifier formats at the routing layer, so invalid IDs never reach your handler. Avoid sending raw database errors to clients. A stable error envelope makes frontend integration predictable and simplifies incident handling: @@ -163,4 +163,4 @@ Finally, if you add auth/validation middleware later, keep the same flow discipl - Primary reference: [gofiber/recipes/gorm-postgres](https://github.com/gofiber/recipes/tree/master/gorm-postgres) -A strong next step is to add validation (see [Binding in Practice](/blog/fiber-v3-binding-in-practice)) and move routes under `/api/v1` with consistent response envelopes (`data`, `error`, `meta`). That gives you a cleaner base before feature count grows. +A strong next step is to add validation (see [Binding in Practice](./2026-02-16-fiber-v3-binding-in-practice.md)) and move routes under `/api/v1` with consistent response envelopes (`data`, `error`, `meta`). That gives you a cleaner base before feature count grows. diff --git a/blog/2026-02-14-spa-delivery-with-fiber-v3.md b/blog/2026-02-14-spa-delivery-with-fiber-v3.md index aac62b34ef25..69efcdbe7c44 100644 --- a/blog/2026-02-14-spa-delivery-with-fiber-v3.md +++ b/blog/2026-02-14-spa-delivery-with-fiber-v3.md @@ -159,6 +159,6 @@ app.Get("/static/*", static.New("./web/build/static", static.Config{ - Primary reference: [gofiber/recipes/react-router](https://github.com/gofiber/recipes/tree/master/react-router) - Alternate reference: [gofiber/recipes/spa](https://github.com/gofiber/recipes/tree/master/spa) -- Related: [Serve Static Files with Fiber v3](/blog/static-server-with-fiber-v3) +- Related: [Serve Static Files with Fiber v3](./2026-02-13-static-server-with-fiber-v3.md) A good next step is to make cache policy explicit per file type and document route ownership between backend and frontend in your service README. If your deployment includes a reverse proxy, test the SPA fallback through the full proxy chain, not just locally. diff --git a/blog/2026-04-12-fiber-v3-error-handling-production.md b/blog/2026-04-12-fiber-v3-error-handling-production.md new file mode 100644 index 000000000000..51243034c3e7 --- /dev/null +++ b/blog/2026-04-12-fiber-v3-error-handling-production.md @@ -0,0 +1,228 @@ +--- +slug: fiber-v3-error-handling-production +title: Error Handling That Doesn't Embarrass You in Production +authors: [fiber-team] +tags: [fiber, v3, error-handling, production, security, go] +description: Build a custom error handler in Fiber v3 that keeps your internals hidden while giving clients useful, structured responses. +--- + +The scariest thing in production is not that errors happen. It is that the wrong information leaks when they do. + +In Fiber v3, the default error handler sends `err.Error()` to the client, which can expose a raw database error, an internal file path, or other library-generated details. Panics are a separate concern: Fiber does not recover them by default, so they crash the process unless you enable the Recover middleware. During development, that kind of visibility is helpful. In production, it is a security incident waiting to happen. An attacker learns your ORM, your table schema, maybe even the specific query that failed. All from a 500 response you never thought anyone would read. + +Fiber v3's error handling is designed around one idea: handlers return errors, and a central handler decides what the client sees. That separation sounds simple, but it changes how you structure error responses across your entire application. + + + +## Why the Default Handler Is Not Enough + +Fiber ships with a default error handler that does the right thing for prototyping: it checks if the error is a `*fiber.Error`, extracts the status code, and sends the error message as plain text. + +```go +var DefaultErrorHandler = func(c fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + var e *fiber.Error + if errors.As(err, &e) { + code = e.Code + } + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + return c.Status(code).SendString(err.Error()) +} +``` + +The problem is `err.Error()`. For a `fiber.Error`, the message is controlled. But for an unexpected error β a database timeout, a nil pointer, a failed file operation β `err.Error()` contains whatever the underlying library decided to put in there. That string goes straight to the client. + +In one real-world incident, a team discovered that their Postgres connection error included the DSN with credentials. The default handler dutifully sent it as the response body. + +## Building a Production Error Handler + +A production error handler needs to do three things: classify the error, log the details internally, and send a sanitized response to the client. + +```go +type APIError struct { + Code int `json:"code"` + Message string `json:"message"` + TraceID string `json:"trace_id,omitempty"` +} + +app := fiber.New(fiber.Config{ + ErrorHandler: func(c fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + message := "An unexpected error occurred" + + var e *fiber.Error + if errors.As(err, &e) { + code = e.Code + message = e.Message + } + + traceID := requestid.FromContext(c) + + // Log the full error internally β never send this to the client + log.Printf("[%s] %d %s %s: %v", + traceID, code, c.Method(), c.Path(), err) + + return c.Status(code).JSON(APIError{ + Code: code, + Message: message, + TraceID: traceID, + }) + }, +}) +``` + +The key insight is the split between `err` (logged, never exposed) and `message` (sent to the client, always controlled). If the error is a `*fiber.Error`, the message was explicitly set by your code. If it is anything else, the client gets a generic message and the real error goes to your logs. + +## Layered Error Types + +For a larger application, you probably want more than just `fiber.Error`. Consider a domain-specific error type that carries both public and private information: + +```go +type AppError struct { + StatusCode int + PublicMsg string + Internal error +} + +func (e *AppError) Error() string { + if e.Internal != nil { + return e.Internal.Error() + } + return e.PublicMsg +} + +func NewNotFound(msg string, internal error) *AppError { + return &AppError{ + StatusCode: fiber.StatusNotFound, + PublicMsg: msg, + Internal: internal, + } +} + +func NewBadRequest(msg string) *AppError { + return &AppError{ + StatusCode: fiber.StatusBadRequest, + PublicMsg: msg, + } +} +``` + +Then your handlers become expressive without leaking internals: + +```go +app.Get("/users/:id", func(c fiber.Ctx) error { + user, err := db.FindUser(c.Params("id")) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return NewNotFound("User not found", err) + } + return err // Will hit the generic "unexpected error" path + } + return c.JSON(user) +}) +``` + +And the error handler checks for your type first: + +```go +ErrorHandler: func(c fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + message := "An unexpected error occurred" + + var appErr *AppError + var fiberErr *fiber.Error + + switch { + case errors.As(err, &appErr): + code = appErr.StatusCode + message = appErr.PublicMsg + if appErr.Internal != nil { + log.Printf("internal: %v", appErr.Internal) + } + case errors.As(err, &fiberErr): + code = fiberErr.Code + message = fiberErr.Message + default: + log.Printf("unhandled: %v", err) + } + + return c.Status(code).JSON(APIError{ + Code: code, + Message: message, + }) +} +``` + +## Don't Forget Panics + +Fiber does not recover from panics by default. If a nil pointer dereference hits your handler, the entire process crashes. The `recover` middleware catches panics and converts them to errors that flow through your error handler: + +```go +import recoverer "github.com/gofiber/fiber/v3/middleware/recover" + +app.Use(recoverer.New()) +``` + +Note the import alias: Go's built-in `recover` keyword conflicts with the package name, so the convention is to import it as `recoverer`. + +With your custom error handler in place, a panic becomes a logged internal error and a clean 500 response instead of a process restart and a confused load balancer. + +One subtlety: the recover middleware should be registered early, before other middleware. If it is registered after the logger, panics in the logger itself will not be caught. + +## Validation Errors as Structured Responses + +Validation failures deserve special treatment. A 400 response that says "bad request" is useless to API consumers. They need to know which field failed and why. + +```go +type ValidationError struct { + Field string `json:"field"` + Message string `json:"message"` +} + +type ValidationErrors struct { + Errors []ValidationError `json:"errors"` +} + +func (e *ValidationErrors) Error() string { + return fmt.Sprintf("%d validation errors", len(e.Errors)) +} +``` + +In the error handler, check for this type and return a 422: + +```go +var validationErr *ValidationErrors +if errors.As(err, &validationErr) { + return c.Status(fiber.StatusUnprocessableEntity).JSON(validationErr) +} +``` + +This pattern works well with `go-playground/validator` or any validation library that returns structured errors. + +## Content Negotiation + +One detail that is easy to miss: not all clients want JSON. If your application serves both an API and HTML pages, the error handler should respect the `Accept` header: + +```go +if c.Accepts("application/json") != "" { + return c.Status(code).JSON(APIError{Code: code, Message: message}) +} +return c.Status(code).Render("error", fiber.Map{ + "Code": code, + "Message": message, +}) +``` + +This avoids the awkward situation where a browser user sees raw JSON on an error page, or an API client receives HTML it cannot parse. + +## Where to Start + +If your application uses the default error handler, start by adding a custom one that does two things: logs the real error and sends a generic message. That single change eliminates the most common information leakage vector. + +Once that is in place, introduce a domain error type for the status codes and messages your handlers use most. You do not need to cover every case on day one. + +## Internal References + +- [Error Handling Guide](/guide/error-handling) +- [Recover Middleware](/middleware/recover) +- [What's New](/whats_new) diff --git a/blog/2026-04-13-fiber-v3-rate-limiting-guide.md b/blog/2026-04-13-fiber-v3-rate-limiting-guide.md new file mode 100644 index 000000000000..2d08f1874fce --- /dev/null +++ b/blog/2026-04-13-fiber-v3-rate-limiting-guide.md @@ -0,0 +1,253 @@ +--- +slug: fiber-v3-rate-limiting-guide +title: "Rate Limiting: Protecting Your API Without Punishing Your Users" +authors: [fiber-team] +tags: [fiber, v3, rate-limiting, limiter, security, api, go] +description: Implement rate limiting in Fiber v3 that actually makes sense β dynamic limits, sliding windows, and per-endpoint strategies. +--- + +Rate limiting is one of those features that every production API needs, nobody enjoys implementing, and most teams get subtly wrong the first time. + +The common mistake is not forgetting rate limiting entirely. It is applying a single global limit and calling it done. Fifty requests per minute, no exceptions, no differentiation. Your power users hit the wall during normal operations. Scrapers figure out the exact limit and stay just below it. Login endpoints get the same allowance as read-only data endpoints. Everyone is equally unhappy. + +Fiber v3's Limiter middleware ships with the primitives to do better: dynamic limits per route, sliding window algorithms, per-user keys, and pluggable storage backends. The trick is knowing when to use which. + + + +## The Five-Minute Setup + +If you have never added rate limiting before, start here. This gets a basic limiter running with sane defaults: + +```go +import ( + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/limiter" +) + +app.Use(limiter.New()) +``` + +That is it. You get 5 requests per minute per IP, a 429 response when the limit is exceeded, and standard `X-RateLimit-*` headers so clients can see their remaining quota. For a weekend project or an internal tool, this might be all you need. + +But production APIs have different requirements. + +## Fixed Window vs. Sliding Window + +The default algorithm is a fixed window: every 60 seconds the counter resets to zero. This works, but it has a well-known burst problem. If a client sends 5 requests at second 59 and 5 more at second 61, they have made 10 requests in 2 seconds despite a "5 per minute" limit. The window boundary creates a loophole. + +The sliding window algorithm smooths this out by considering the previous window's traffic: + +```go +app.Use(limiter.New(limiter.Config{ + Max: 20, + Expiration: 30 * time.Second, + LimiterMiddleware: limiter.SlidingWindow{}, +})) +``` + +The math is straightforward: it takes the number of requests from the previous window, weights them by how much of the current window has elapsed, and adds the current window's count. If the previous window had 15 requests and we are 40% into the current window, the effective count is `15 * 0.6 + currentCount`. This eliminates the boundary burst without adding meaningful overhead. + +Use fixed windows for simple use cases where exact precision does not matter. Use sliding windows for auth endpoints, payment APIs, or anywhere burst protection matters. + +## Dynamic Limits: Not All Endpoints Are Equal + +A login endpoint that accepts passwords should have a much tighter limit than a product catalog that serves public data. With `MaxFunc`, you can calculate limits per request: + +```go +app.Use(limiter.New(limiter.Config{ + MaxFunc: func(c fiber.Ctx) int { + switch { + case strings.HasPrefix(c.Path(), "/auth"): + return 5 // strict: 5 per window for auth + case strings.HasPrefix(c.Path(), "/api/v1/search"): + return 60 // generous: search is read-only + default: + return 20 // reasonable default + } + }, + Expiration: time.Minute, +})) +``` + +A more sophisticated approach uses the authenticated user's tier: + +```go +MaxFunc: func(c fiber.Ctx) int { + tier, ok := c.Locals("user_tier").(string) + if !ok { + return 10 // unauthenticated baseline + } + switch tier { + case "enterprise": + return 1000 + case "pro": + return 200 + default: + return 50 + } +}, +``` + +This requires your auth middleware to run before the limiter. Registration order matters β middleware executes in the order you call `app.Use`. + +## Dynamic Expiration Windows + +Similarly, `ExpirationFunc` lets you vary the time window per request. A login endpoint might use a longer window to prevent slow brute force attacks: + +```go +app.Use(limiter.New(limiter.Config{ + Max: 10, + ExpirationFunc: func(c fiber.Ctx) time.Duration { + if c.Path() == "/login" || c.Path() == "/reset-password" { + return 5 * time.Minute // 10 attempts per 5 minutes + } + return 1 * time.Minute // 10 per minute everywhere else + }, +})) +``` + +## Better Key Generation + +The default key generator uses the client IP. Behind a load balancer or CDN, every request might appear to come from the same IP unless Fiber knows which proxy headers to trust. + +The correct approach is to configure Fiber's built-in proxy trust settings so `c.IP()` resolves the real client IP safely: + +```go +app := fiber.New(fiber.Config{ + TrustProxy: true, + TrustProxyConfig: fiber.TrustProxyConfig{ + Proxies: []string{"10.0.0.0/8", "172.16.0.0/12"}, + }, + ProxyHeader: "X-Forwarded-For", +}) +``` + +With that in place, the default `KeyGenerator` using `c.IP()` already does the right thing. Do not parse `X-Forwarded-For` manually β clients can spoof that header to bypass IP-based limits unless your proxy overwrites it. + +For authenticated APIs, a better key is the user ID. This means the limit follows the account, not the IP, which is the correct behavior for mobile users who switch networks: + +```go +KeyGenerator: func(c fiber.Ctx) string { + if userID, ok := c.Locals("user_id").(string); ok { + return "user:" + userID + } + return "ip:" + c.IP() +}, +``` + +The `user:` and `ip:` prefixes prevent collisions between user IDs and IP addresses. + +## Custom Limit-Reached Responses + +When a client hits the limit, the default handler returns a bare 429 status. API consumers deserve more: + +```go +LimitReached: func(c fiber.Ctx) error { + retryAfter := c.GetRespHeader("Retry-After") + return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ + "error": "rate_limit_exceeded", + "message": "Too many requests. Please slow down.", + "retry_after": retryAfter, + }) +}, +``` + +This gives clients machine-readable information they can use to implement backoff correctly. + +## Skipping Certain Requests + +Not everything should count. Health check endpoints should never be rate limited β a monitoring system polling `/healthz` every second is expected behavior: + +```go +app.Use(limiter.New(limiter.Config{ + Max: 30, + Expiration: time.Minute, + Next: func(c fiber.Ctx) bool { + // Skip health checks and internal IPs + if c.Path() == "/healthz" || c.Path() == "/readyz" { + return true + } + return c.IP() == "10.0.0.1" // internal monitoring + }, +})) +``` + +You can also skip failed requests so that server errors do not eat a user's quota: + +```go +SkipFailedRequests: true, // status >= 400 won't count +``` + +Or the opposite β only count failed requests to detect brute force patterns without penalizing normal usage: + +```go +SkipSuccessfulRequests: true, // only failed attempts count +``` + +## Distributed Rate Limiting with Redis + +The in-memory store works for single instances but falls apart behind a load balancer. If you have three instances each allowing 20 requests per minute, a client gets 60 effective requests. + +Plugging in Redis makes the limit shared across instances: + +```go +import "github.com/gofiber/storage/redis/v3" + +store := redis.New(redis.Config{ + Host: "redis.internal", + Port: 6379, +}) + +app.Use(limiter.New(limiter.Config{ + Max: 20, + Expiration: time.Minute, + Storage: store, +})) +``` + +Any storage backend from Fiber's [storage package](https://github.com/gofiber/storage) works β Redis, Memcache, DynamoDB, Postgres. The interface is the same. + +## Per-Route Limiters + +Sometimes a global middleware is not granular enough. You can apply different limiter instances to different route groups: + +```go +// Strict limiter for auth routes +authLimiter := limiter.New(limiter.Config{ + Max: 5, + Expiration: 5 * time.Minute, + LimiterMiddleware: limiter.SlidingWindow{}, +}) + +// Relaxed limiter for public API +apiLimiter := limiter.New(limiter.Config{ + Max: 100, + Expiration: time.Minute, +}) + +auth := app.Group("/auth") +auth.Use(authLimiter) +auth.Post("/login", loginHandler) +auth.Post("/reset", resetHandler) + +api := app.Group("/api") +api.Use(apiLimiter) +api.Get("/products", listProducts) +api.Get("/products/:id", getProduct) +``` + +This is cleaner than a single middleware with complex conditional logic, and makes the limits obvious in code review. + +## Where to Start + +If your API has no rate limiting at all, add the one-line default and deploy it. That alone protects against accidental abuse and simple denial-of-service attempts. + +Next, identify your sensitive endpoints β login, password reset, payment β and give them stricter limits with a sliding window. Then add Redis storage when you scale past a single instance. + +The goal is not to block legitimate users. It is to make your API predictable under load and expensive to abuse. + +## Internal References + +- [Limiter Middleware](/middleware/limiter) +- [Storage Package](https://github.com/gofiber/storage) +- [What's New](/whats_new) diff --git a/blog/2026-04-14-fiber-v3-testing-patterns.md b/blog/2026-04-14-fiber-v3-testing-patterns.md new file mode 100644 index 000000000000..1e396d858c14 --- /dev/null +++ b/blog/2026-04-14-fiber-v3-testing-patterns.md @@ -0,0 +1,290 @@ +--- +slug: fiber-v3-testing-patterns +title: "Testing Fiber Apps: The Patterns Nobody Talks About" +authors: [fiber-team] +tags: [fiber, v3, testing, go, patterns, best-practices] +description: Go beyond basic handler tests with patterns for middleware chains, error handlers, route groups, and integration testing in Fiber v3. +--- + +Every Fiber tutorial shows you how to test a single GET handler. Create an app, register a route, call `app.Test()`, check the status code. Done. + +Then you try to test something real β a middleware chain where auth runs before validation, a custom error handler that renders different responses based on content type, a route group with shared state β and the tutorial patterns fall apart. The handler works in isolation but fails when composed. The test passes with a hardcoded body but breaks when you add a request ID middleware that changes the response shape. + +Testing Fiber applications well requires patterns that match how Fiber applications actually work: as compositions of handlers, middleware, and configuration that interact in specific ways. + + + +## The Basics: app.Test() + +Fiber's `Test` method is the foundation. It takes a standard `*http.Request` and returns a standard `*http.Response`, no actual network listener required: + +```go +func TestGetUser(t *testing.T) { + app := fiber.New() + app.Get("/users/:id", func(c fiber.Ctx) error { + return c.JSON(fiber.Map{"id": c.Params("id")}) + }) + + req := httptest.NewRequest(http.MethodGet, "/users/42", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, 200, resp.StatusCode) +} +``` + +This works because Fiber internally creates a `fasthttp.RequestCtx` from your `http.Request`, runs the full router and middleware stack, and converts the result back. No port binding, no goroutine leaks, no flaky tests from port conflicts. + +The default timeout is 1 second. For handlers that do real work, like database queries in integration tests, increase it: + +```go +resp, err := app.Test(req, fiber.TestConfig{ + Timeout: 5 * time.Second, +}) +``` + +For unit tests where you know the handler returns immediately, disable the timeout entirely: + +```go +resp, err := app.Test(req, fiber.TestConfig{ + Timeout: 0, +}) +``` + +## Testing Middleware in Isolation + +A middleware function is just a handler that calls `c.Next()`. You can test it by creating a minimal app with the middleware and a dummy handler: + +```go +func TestAuthMiddleware(t *testing.T) { + app := fiber.New() + app.Use(authMiddleware) + app.Get("/protected", func(c fiber.Ctx) error { + return c.SendString("ok") + }) + + // Without token β should get 401 + req := httptest.NewRequest(http.MethodGet, "/protected", nil) + resp, _ := app.Test(req) + assert.Equal(t, 401, resp.StatusCode) + + // With valid token β should pass through + req = httptest.NewRequest(http.MethodGet, "/protected", nil) + req.Header.Set("Authorization", "Bearer valid-token") + resp, _ = app.Test(req) + assert.Equal(t, 200, resp.StatusCode) +} +``` + +The important thing is that this tests the middleware in context β with a real router, real header parsing, and real `c.Next()` propagation. You are testing behavior, not implementation. + +## Testing Middleware Composition + +The bugs that matter most happen between middleware, not inside them. When auth, rate limiting, and request ID middleware interact, the order and error propagation matter. + +Test the composition explicitly: + +```go +func TestMiddlewareChain(t *testing.T) { + app := fiber.New() + app.Use(requestid.New()) + app.Use(authMiddleware) + app.Use(rateLimiter) + app.Get("/api/data", dataHandler) + + // An unauthenticated request should fail at auth, + // not at the rate limiter + req := httptest.NewRequest(http.MethodGet, "/api/data", nil) + resp, _ := app.Test(req) + assert.Equal(t, 401, resp.StatusCode) + + // The response should still have a request ID, + // because requestid runs before auth + assert.NotEmpty(t, resp.Header.Get("X-Request-Id")) +} +``` + +This catches the common bug where a middleware short-circuits and skips downstream middleware that should still run. Request ID middleware should always add a header, even on error responses. If your tests only check the happy path, you will never catch this. + +## Testing Custom Error Handlers + +Custom error handlers are critical infrastructure, but they are rarely tested directly. Build a test that triggers different error types and verifies the response: + +```go +func TestCustomErrorHandler(t *testing.T) { + app := fiber.New(fiber.Config{ + ErrorHandler: myErrorHandler, + }) + + // Route that returns a fiber.Error + app.Get("/not-found", func(c fiber.Ctx) error { + return fiber.NewError(404, "Page not found") + }) + + // Route that returns an unexpected error + app.Get("/crash", func(c fiber.Ctx) error { + return errors.New("database connection failed: host=db.internal") + }) + + // Known error: should return the controlled message + req := httptest.NewRequest(http.MethodGet, "/not-found", nil) + resp, _ := app.Test(req) + body, _ := io.ReadAll(resp.Body) + assert.Equal(t, 404, resp.StatusCode) + assert.Contains(t, string(body), "Page not found") + + // Unknown error: should NOT leak the internal message + req = httptest.NewRequest(http.MethodGet, "/crash", nil) + resp, _ = app.Test(req) + body, _ = io.ReadAll(resp.Body) + assert.Equal(t, 500, resp.StatusCode) + assert.NotContains(t, string(body), "database connection failed") + assert.NotContains(t, string(body), "db.internal") +} +``` + +That last assertion β checking that the response does *not* contain the internal error β is the most important test in your entire error handling suite. It catches information leakage. + +## Testing Route Groups + +When you have nested route groups with group-specific middleware, test them as a unit: + +```go +func setupAPIRoutes(app *fiber.App) { + api := app.Group("/api") + api.Use(apiKeyAuth) + + v1 := api.Group("/v1") + v1.Get("/users", listUsers) + v1.Post("/users", createUser) + + v2 := api.Group("/v2") + v2.Get("/users", listUsersV2) +} + +func TestAPIRouteGroup(t *testing.T) { + app := fiber.New() + setupAPIRoutes(app) + + tests := []struct { + name string + path string + method string + apiKey string + wantStatus int + }{ + {"v1 without key", "/api/v1/users", "GET", "", 401}, + {"v1 with key", "/api/v1/users", "GET", "valid-key", 200}, + {"v2 with key", "/api/v2/users", "GET", "valid-key", 200}, + {"no version", "/api/users", "GET", "valid-key", 404}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(tt.method, tt.path, nil) + if tt.apiKey != "" { + req.Header.Set("X-API-Key", tt.apiKey) + } + resp, _ := app.Test(req) + assert.Equal(t, tt.wantStatus, resp.StatusCode) + }) + } +} +``` + +The key pattern is extracting route setup into a function that accepts `*fiber.App`. This lets your tests use the exact same setup as your production code without duplicating route definitions. + +## Testing JSON Request Bodies + +POST and PUT handlers need request bodies. Use `strings.NewReader` or `bytes.NewBuffer`: + +```go +func TestCreateUser(t *testing.T) { + app := fiber.New() + app.Post("/users", createUserHandler) + + body := `{"name": "Alice", "email": "alice@example.com"}` + req := httptest.NewRequest( + http.MethodPost, + "/users", + strings.NewReader(body), + ) + req.Header.Set("Content-Type", "application/json") + + resp, _ := app.Test(req) + assert.Equal(t, 201, resp.StatusCode) + + var result map[string]any + json.NewDecoder(resp.Body).Decode(&result) + assert.Equal(t, "Alice", result["name"]) +} +``` + +The `Content-Type` header matters. Without it, Fiber's body parser may not parse the JSON correctly. + +## Testing Response Headers + +Rate limit headers, CORS headers, cache headers β these are part of your API contract. Test them explicitly: + +```go +func TestRateLimitHeaders(t *testing.T) { + app := fiber.New() + app.Use(limiter.New(limiter.Config{ + Max: 5, + Expiration: time.Minute, + })) + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("ok") + }) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + resp, _ := app.Test(req) + + assert.Equal(t, "5", resp.Header.Get("X-RateLimit-Limit")) + assert.Equal(t, "4", resp.Header.Get("X-RateLimit-Remaining")) +} +``` + +## A Helper That Pays for Itself + +If you find yourself writing the same `httptest.NewRequest` / `app.Test` / `io.ReadAll` sequence in every test, extract it once: + +```go +func testRequest(t *testing.T, app *fiber.App, method, path string, opts ...func(*http.Request)) (int, string) { + t.Helper() + req := httptest.NewRequest(method, path, nil) + for _, opt := range opts { + opt(req) + } + resp, err := app.Test(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + return resp.StatusCode, string(body) +} + +// Usage: +status, body := testRequest(t, app, "GET", "/users/42", + func(r *http.Request) { + r.Header.Set("Authorization", "Bearer token") + }, +) +``` + +This keeps tests focused on what matters: the inputs, the expected status, and the expected body. + +## Where to Start + +If your test suite only has happy-path tests, add error-path tests first. Test that unauthenticated requests get 401, that invalid input gets 422, and that internal errors return generic messages without leaking details. + +Then test your middleware composition. Register the same middleware stack you use in production and verify that they interact correctly. These are the tests that catch the bugs you will actually ship. + +## Internal References + +- [App Test Method](/api/app#test) +- [Error Handling Guide](/guide/error-handling) +- [What's New](/whats_new) diff --git a/blog/2026-04-15-fiber-v3-structured-logging.md b/blog/2026-04-15-fiber-v3-structured-logging.md new file mode 100644 index 000000000000..fb16c94f6a78 --- /dev/null +++ b/blog/2026-04-15-fiber-v3-structured-logging.md @@ -0,0 +1,223 @@ +--- +slug: fiber-v3-structured-logging +title: "From fmt.Println to Production Logging in Fiber v3" +authors: [fiber-team] +tags: [fiber, v3, logging, observability, middleware, go] +description: Move beyond basic request logging to structured, queryable, production-grade observability with Fiber v3's Logger middleware. +--- + +There is a moment in every project's life where someone greps the production logs for a bug report and realizes that `fmt.Println("got here")` is the only evidence of what happened. The request came in, something went wrong, and the logs show a status code with no context about which user, which endpoint, or which upstream service was involved. + +Logging sounds boring until it is 2 AM and your only debugging tool is `kubectl logs`. At that point, the difference between a flat text line and a structured JSON object with a request ID, latency, and user context is the difference between finding the bug in five minutes and finding it in two hours. + +Fiber v3's Logger middleware is designed to bridge that gap without requiring you to rewrite your application. + + + +## The Default: Good Enough to Start + +Out of the box, the Logger middleware gives you a single line per request with the most useful fields: + +```go +app.Use(logger.New()) +// Output: [15:04:05] 127.0.0.1 200 - 1.234ms GET /api/users +``` + +That is timestamp, IP, status code, latency, method, and path. For local development and small services, this is fine. Register it early β routes added after the logger are logged, routes added before it are not. + +```go +app := fiber.New() +app.Use(logger.New()) // register first +app.Get("/", handler) // this route will be logged +``` + +## Structured JSON Logging + +The moment your logs go to an aggregation system β Elasticsearch, Loki, CloudWatch, Datadog β you need structured output. Flat text requires fragile regex to query. JSON is queryable by default. + +Fiber provides a built-in JSON format: + +```go +app.Use(logger.New(logger.Config{ + Format: logger.JSONFormat, +})) +``` + +This produces output like: + +```json +{"time":"15:04:05","ip":"127.0.0.1","method":"GET","url":"/api/users","status":200,"bytesSent":1234} +``` + +For teams using Elastic Common Schema, there is a dedicated ECS format: + +```go +app.Use(logger.New(logger.Config{ + Format: logger.ECSFormat, +})) +``` + +This outputs logs that Elasticsearch, Kibana, and the Elastic APM can ingest without any parsing rules. The schema includes `@timestamp`, `ecs.version`, `client.ip`, `http.request.method`, and `http.response.status_code` β all in the right places. + +## Custom Formats for Your Stack + +The predefined formats cover common cases, but most teams need something specific. Fiber's format string uses `${tag}` placeholders that you can arrange freely: + +```go +app.Use(logger.New(logger.Config{ + Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error}\n", + TimeFormat: "2006-01-02T15:04:05Z07:00", + TimeZone: "UTC", +})) +``` + +For structured JSON with custom fields, build the JSON string manually: + +```go +app.Use(logger.New(logger.Config{ + Format: `{"ts":"${time}","method":"${method}","path":"${path}",` + + `"status":${status},"latency":"${latency}","ip":"${ip}",` + + `"bytes_sent":${bytesSent},"bytes_recv":${bytesReceived}}` + "\n", + TimeFormat: time.RFC3339, + TimeZone: "UTC", +})) +``` + +The available tags cover nearly everything you would want: `${pid}`, `${ip}`, `${ips}`, `${host}`, `${method}`, `${path}`, `${url}`, `${ua}`, `${latency}`, `${status}`, `${bytesSent}`, `${bytesReceived}`, `${route}`, `${error}`, `${body}`, `${resBody}`, `${reqHeaders}`, `${queryParams}`. + +You can also reference specific headers, cookies, query params, and locals: + +```go +Format: `${reqHeader:X-Request-Id} ${cookie:session_id} ${query:page} ${locals:user_id}` + "\n" +``` + +## Adding Request IDs + +The single most useful thing you can add to your logs is a request ID. It lets you correlate every log line, error, and downstream service call to a single request. + +Combine the RequestID middleware with a custom logger tag: + +```go +app.Use(requestid.New()) +app.Use(logger.New(logger.Config{ + CustomTags: map[string]logger.LogFunc{ + "request_id": func(output logger.Buffer, c fiber.Ctx, data *logger.Data, _ string) (int, error) { + return output.WriteString(requestid.FromContext(c)) + }, + }, + Format: `{"request_id":"${request_id}","method":"${method}","path":"${path}","status":${status}}` + "\n", +})) +``` + +Now every log line carries the request ID. When a user reports a problem, they send you the ID from the response header and you can trace everything that happened. + +## The Done Callback: Conditional Alerting + +The `Done` callback fires after each log line is written. This is useful for routing specific events to different systems without building a separate middleware: + +```go +app.Use(logger.New(logger.Config{ + TimeFormat: time.RFC3339Nano, + Done: func(c fiber.Ctx, logString []byte) { + if c.Response().StatusCode() >= 500 { + alerting.SendToSlack(logString) + } + }, +})) +``` + +This is not a replacement for a proper alerting pipeline, but it is a pragmatic way to get notified about server errors immediately without deploying additional infrastructure. + +You can also use it for audit logging β writing specific requests to a separate file or database: + +```go +Done: func(c fiber.Ctx, logString []byte) { + if strings.HasPrefix(c.Path(), "/admin") { + auditLog.Write(logString) + } +}, +``` + +## Skipping Noise + +Health check endpoints, metrics scraping, and Kubernetes probes generate enormous amounts of log traffic with zero diagnostic value. The `Skip` function lets you filter them out: + +```go +app.Use(logger.New(logger.Config{ + Skip: func(c fiber.Ctx) bool { + return c.Path() == "/healthz" || + c.Path() == "/readyz" || + c.Path() == "/metrics" + }, +})) +``` + +The difference between `Next` and `Skip` matters here. `Next` skips the middleware entirely β the request is not logged and no log processing happens. `Skip` still processes the request through the handler chain but suppresses the log output. For performance, prefer `Next` when you do not need any log processing for skipped routes. + +## Writing to Files + +For environments without a log aggregator, writing to files is still the right approach: + +```go +f, err := os.OpenFile("./access.log", + os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) +if err != nil { + log.Fatal(err) +} +defer f.Close() + +app.Use(logger.New(logger.Config{ + Stream: f, + DisableColors: true, // no ANSI in files +})) +``` + +The `DisableColors` flag is important. Without it, your log files fill up with ANSI escape codes that make them unreadable in plain text editors and break log parsing tools. + +## Integrating with Zap, Zerolog, and Friends + +Most Go teams already have a logging library. Fiber's `LoggerToWriter` adapter lets you pipe the logger middleware output into any logger that satisfies an interface: + +```go +import ( + fiberzap "github.com/gofiber/contrib/v3/zap" + "github.com/gofiber/fiber/v3/log" + "github.com/gofiber/fiber/v3/middleware/logger" +) + +zapLogger := fiberzap.NewLogger(fiberzap.LoggerConfig{ + ExtraKeys: []string{"request_id"}, +}) + +app.Use(logger.New(logger.Config{ + Stream: logger.LoggerToWriter(zapLogger, log.LevelDebug), +})) +``` + +Note the import path: Fiber v3 contrib packages live under `github.com/gofiber/contrib/v3/` and drop the `fiber` prefix from their names. So `fiberzap/v2` becomes `contrib/v3/zap`, `fiberzerolog` becomes `contrib/v3/zerolog`, and so on. + +This means Fiber's request logs go through your existing log pipeline, with your existing log levels, formatters, and sinks. You do not need to maintain two separate logging systems. + +The same pattern works with `contrib/v3/zerolog` and any logger that implements Fiber's `AllLogger` interface. + +## Common Pitfalls + +**Logging request bodies in production.** The `${body}` tag exists for debugging, but enabling it in production means every POST body β including passwords, tokens, and PII β ends up in your logs. Only use it in development or behind a feature flag. + +**Logging response bodies.** Same problem. The `${resBody}` tag is useful for debugging, but it can log sensitive data and dramatically increase log volume. + +**Forgetting timezone.** The default timezone is `"Local"`, which means your logs use whatever the server's timezone is. In a distributed system, different servers might log in different timezones. Always set `TimeZone: "UTC"` for production. + +**Registration order.** The logger only captures routes registered after it. If you register a health check route before the logger, it will not be logged β which might actually be what you want. + +## Where to Start + +If you are using `fmt.Println` for debugging, switch to the Logger middleware with the default format. You get latency and status codes immediately, which eliminates the most common "what happened?" questions. + +Next, switch to JSON format and add a request ID. This alone makes your logs queryable and correlatable, which is 80% of what you need for production debugging. + +## Internal References + +- [Logger Middleware](/middleware/logger) +- [RequestID Middleware](/middleware/requestid) +- [What's New](/whats_new) diff --git a/blog/2026-04-16-fiber-v3-express-style-handlers.md b/blog/2026-04-16-fiber-v3-express-style-handlers.md new file mode 100644 index 000000000000..d132d295fbf0 --- /dev/null +++ b/blog/2026-04-16-fiber-v3-express-style-handlers.md @@ -0,0 +1,224 @@ +--- +slug: fiber-v3-express-style-handlers +title: "Express-Style Handlers in Go: Fiber's Adapter That Nobody Expected" +authors: [fiber-team] +tags: [fiber, v3, handlers, express, adapter, migration, go] +description: Fiber v3 accepts seventeen different handler signatures including Express-style, net/http, and fasthttp β here is when and why to use each. +--- + +If you have ever tried to migrate a project from Express.js to Go, you know the friction. It is not the language syntax or the type system. It is that every HTTP handler follows a completely different convention. Express gives you `(req, res, next)`. Go's standard library gives you `(w, r)`. Fiber gives you `(c) error`. The logic is the same, but the shape is different, and reshaping hundreds of handlers is tedious, error-prone work. + +Fiber v3 decided to stop pretending this is not a problem. Its handler adapter accepts seventeen different function signatures β from Fiber-native to Express-style callbacks to raw `net/http` and `fasthttp` handlers. You can mix them in the same application without manual wrapping. + +This sounds like magic. It is actually a carefully designed type switch in `adapter.go` that performs this adaptation at runtime when routes are registered, instead of forcing you to do it by hand. + + + +## The Fiber-Native Way + +The canonical Fiber handler is a function that takes a context and returns an error: + +```go +app.Get("/", func(c fiber.Ctx) error { + return c.SendString("hello") +}) +``` + +This is case 1 in the adapter. If you are writing a new Fiber application from scratch, this is the only handler shape you need to know. The error return is important β it flows to the central error handler, which means error handling is consistent across your entire application. + +Case 2 is the error-less variant: + +```go +app.Get("/", func(c fiber.Ctx) { + c.SendString("hello") +}) +``` + +Fiber runs the function and treats it as if it returned `nil`. This is convenient for handlers that cannot fail, like health checks, but you lose the ability to propagate errors. Use it sparingly. + +## Express-Style Handlers: The Migration Bridge + +This is where it gets interesting. Fiber v3 accepts handlers with `(req, res)` and `(req, res, next)` signatures that mirror Express.js patterns: + +```go +// Like Express: app.get('/', (req, res) => { ... }) +app.Get("/", func(req fiber.Req, res fiber.Res) error { + return res.SendString("hello from Express-style") +}) +``` + +The `fiber.Req` and `fiber.Res` types are views into the same underlying context, split into request and response concerns. This separation is natural for developers coming from Express, where `req` and `res` are distinct objects. + +The middleware pattern translates directly too: + +```go +// Like Express: app.use((req, res, next) => { ... }) +app.Use(func(req fiber.Req, res fiber.Res, next func() error) error { + start := time.Now() + err := next() + log.Printf("%s took %v", req.Path(), time.Since(start)) + return err +}) +``` + +There are ten Express-style variants (cases 3 through 12), covering every combination of: +- With or without `next` callback +- `next` that takes no arguments, `next(error)`, or `next(error) error` +- Handler with or without error return + +This means you can translate most Express middleware patterns directly without redesigning the error flow. + +## Error Propagation in Next Callbacks + +The different `next` signatures handle errors differently, and understanding this matters for middleware: + +```go +// next() error β call next, get the downstream error back +app.Use(func(req fiber.Req, res fiber.Res, next func() error) error { + err := next() + if err != nil { + log.Printf("downstream error: %v", err) + } + return err +}) + +// next(error) β pass an error to short-circuit the chain +app.Use(func(req fiber.Req, res fiber.Res, next func(error)) { + if !isAuthorized(req) { + next(fiber.ErrUnauthorized) + return + } + next(nil) // continue the chain +}) + +// next(error) error β both send and receive errors +app.Use(func(req fiber.Req, res fiber.Res, next func(error) error) error { + err := next(nil) + if err != nil { + // Post-processing after downstream error + return res.Status(500).SendString("something went wrong") + } + return nil +}) +``` + +If your handler returns an error *and* the next callback recorded an error, Fiber prioritizes the handler's return value. This prevents confusing situations where a downstream error silently overrides a handler's explicit response. + +## Reusing net/http Handlers + +Go's standard library ecosystem is enormous. There are handlers for Prometheus metrics, pprof profiling, OAuth callbacks, and hundreds of other things that accept `http.ResponseWriter` and `*http.Request`. Fiber can mount them directly: + +```go +import "net/http" + +// http.HandlerFunc works directly +app.Get("/metrics", http.HandlerFunc(metricsHandler)) + +// http.Handler interface works too +app.Get("/debug/*", pprofMux) + +// Raw function signature +app.Get("/legacy", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("from net/http")) +}) +``` + +There is an important caveat: these handlers go through `fasthttpadaptor`, which adds overhead compared to native Fiber handlers. More importantly, they do not receive `fiber.Ctx`, so they cannot call `c.Next()` and always terminate the handler chain. They are meant for leaf handlers, not middleware. + +If you need a `net/http` middleware to participate in the Fiber chain, use the [Adaptor middleware](/middleware/adaptor) instead, which provides full bidirectional conversion. + +## Mounting fasthttp Handlers + +Since Fiber runs on fasthttp internally, fasthttp handlers integrate with zero overhead: + +```go +import "github.com/valyala/fasthttp" + +app.Get("/fast", func(ctx *fasthttp.RequestCtx) { + ctx.SetStatusCode(200) + ctx.SetBody([]byte("zero overhead")) +}) + +// With error return +app.Get("/fast-err", func(ctx *fasthttp.RequestCtx) error { + if ctx.QueryArgs().Peek("fail") != nil { + return errors.New("intentional failure") + } + ctx.SetStatusCode(200) + return nil +}) +``` + +This is useful when you have existing fasthttp code that you are migrating into a Fiber application, or when you need direct access to fasthttp features that Fiber does not expose. + +## A Real Migration: Express to Fiber + +Suppose you have an Express.js application with middleware like this: + +```javascript +// Express.js +app.use((req, res, next) => { + const start = Date.now(); + res.on('finish', () => { + console.log(`${req.method} ${req.path} - ${Date.now() - start}ms`); + }); + next(); +}); + +app.get('/users/:id', (req, res) => { + const user = findUser(req.params.id); + if (!user) { + return res.status(404).json({ error: 'not found' }); + } + res.json(user); +}); +``` + +The Fiber v3 equivalent, using Express-style handlers, looks like this: + +```go +// Fiber v3 with Express-style handlers +app.Use(func(req fiber.Req, res fiber.Res, next func() error) error { + start := time.Now() + err := next() + log.Printf("%s %s - %v", req.Method(), req.Path(), time.Since(start)) + return err +}) + +app.Get("/users/:id", func(req fiber.Req, res fiber.Res) error { + user := findUser(req.Params("id")) + if user == nil { + return res.Status(404).JSON(fiber.Map{"error": "not found"}) + } + return res.JSON(user) +}) +``` + +The structure is nearly identical. The shapes match. A developer who knows Express can read this code and understand what it does without learning Fiber's conventions first. Over time, as the team gets comfortable, they can gradually migrate to idiomatic `fiber.Ctx` handlers β or not, if the Express style works for them. + +## When to Use Which + +**Use `func(fiber.Ctx) error`** for new code. It is the most idiomatic, has the best tooling support, and gives you full access to Fiber's context API. + +**Use Express-style `(req, res)` handlers** when migrating from Express.js or when your team thinks in terms of separate request and response objects. There is no performance penalty β the adapter resolves at application startup. + +**Use `net/http` handlers** to mount existing Go ecosystem tools (Prometheus, pprof, OAuth libraries) without writing adapter code. Accept the overhead trade-off for leaf handlers. + +**Use `fasthttp` handlers** for existing fasthttp code or when you need direct access to the underlying request context. This is an escape hatch, not a primary pattern. + +**Avoid mixing styles within the same route group.** If your `/api/v1` group uses Fiber-native handlers, do not introduce Express-style handlers in the same group. Consistency within a module matters more than using the "best" handler type everywhere. + +## Where to Start + +If you are starting a new project, use Fiber-native handlers exclusively. There is no reason to use the adapter when you have no legacy code. + +If you are migrating from Express, start by converting your route definitions and middleware using Express-style handlers. Get the application working first, then convert to idiomatic Fiber handlers one module at a time during regular refactoring. + +If you are integrating Go ecosystem tools, mount them directly and move on. The adapter handles the complexity. + +## Internal References + +- [App Route Handlers](/api/app#route-handlers) +- [Adaptor Middleware](/middleware/adaptor) +- [What's New](/whats_new) diff --git a/project-words.txt b/project-words.txt new file mode 100644 index 000000000000..64ce4e39fbe5 --- /dev/null +++ b/project-words.txt @@ -0,0 +1,28 @@ +gofiber +fasthttp +Fasthttp +Prefork +keyauth +gorm +GORM +healthz +readyz +zerolog +Zerolog +fiberzap +fiberzerolog +fasthttpadaptor +valyala +logrocket +tutorialedge +yongweilun +ShΓ³stak +Nnakwue +JΓ³zsef +Sallai +apiv +addbook +allbooks +mtype +Bearertoken +thinked diff --git a/src/components/home/Features.module.scss b/src/components/home/Features.module.scss index e19575577209..5c7deea3e7c0 100644 --- a/src/components/home/Features.module.scss +++ b/src/components/home/Features.module.scss @@ -1,4 +1,4 @@ -/* Section-Bands mit alternierenden HintergrΓΌnden β rein via CSS */ +/* Section bands with alternating backgrounds β pure CSS */ .features { padding: 0; } .bands { display: block; } diff --git a/src/theme/DocVersionBanner/index.tsx b/src/theme/DocVersionBanner/index.tsx index 6d8c45f5a96b..b06e0ce5f102 100644 --- a/src/theme/DocVersionBanner/index.tsx +++ b/src/theme/DocVersionBanner/index.tsx @@ -73,7 +73,7 @@ export default function DocVersionBannerWrapper(props: Props): JSX.Element | nul {expectedPathForCurrentVersion?.path &&