Building a Versioned Fiber API with Shared Middleware and Context-Safe Handlers
Fiber recycles its Context object for zero-allocation performance. Build versioned APIs with grouped middleware while copying request data before spawning goroutines to prevent data races.
20 Aug 2025, 06:13 UTC

When you need to expose multiple API versions in Fiber, grouped routing lets you attach shared middleware to path prefixes. The trap is Fiber’s zero-allocation design: the Context object is pooled and recycled, so passing it directly to goroutines or storing it globally introduces data races that the race detector will flag.
Use Group to scope middleware to /v1 and /v2, call Next() to continue the chain, and copy primitive values out of the Context before any concurrent work.
Desired outcome
A Fiber application that serves /v1 and /v2 route groups behind a shared middleware layer, with handlers that safely use request-scoped data without triggering pooling-related data races.
Prerequisites
- A Go toolchain installed. Verify with
go versionin your terminal. - A module initialized: run
go mod init example.com/fiber-apiin an empty project directory. - Fiber added: run
go get github.com/gofiber/fiber/v2in the same directory. - Basic familiarity with Go functions and HTTP handlers.
Procedure
1. Initialize the application and shared middleware
Fiber middleware is a function with the signature func(*fiber.Ctx) error. To modify a header and continue the chain, set the header and call c.Next(). Code after Next() runs as post-processing after the route handler returns.
package main
import (
"log"
"github.com/gofiber/fiber/v2"
)
// RequestLogger runs before and after versioned handlers.
func RequestLogger(c *fiber.Ctx) error {
// Copy primitives before any potential async boundary.
method := c.Method()
path := c.Path()
err := c.Next()
log.Printf("%s %s completed", method, path)
return err
}
func main() {
app := fiber.New()
// Apply shared middleware to every route.
app.Use(RequestLogger)
// v1 group inherits the shared middleware.
v1 := app.Group("/v1")
v1.Get("/users/:id", func(c *fiber.Ctx) error {
id := c.Params("id") // string copy, safe to reuse
return c.JSON(fiber.Map{
"version": "v1",
"id": id,
})
})
// v2 group also inherits the shared middleware.
v2 := app.Group("/v2")
v2.Get("/users/:id", func(c *fiber.Ctx) error {
id := c.Params("id")
return c.JSON(fiber.Map{
"version": "v2",
"id": id,
})
})
log.Fatal(app.Listen(":3000"))
}
2. Apply middleware to versioned groups
app.Group("/v1") returns a router group. Middleware registered with Use on the app applies to all groups. If you need version-specific middleware, pass it as an argument to Group or call v1.Use().
3. Read route parameters safely
Route parameters such as :id are retrieved with c.Params("id"). The returned value is a string, which you can safely pass into local variables, JSON responses, or goroutines.
4. Keep goroutines free of Context references
Fiber reuses *fiber.Ctx objects across requests to keep allocations low. If you pass the pointer to a goroutine, the request may finish and the object may be recycled before the goroutine reads it, producing a data race.
| Pattern | Safe? | Reason |
|---|---|---|
Passing *fiber.Ctx to a goroutine | No | Object is recycled after the response |
Copying c.Params("id") to a local string | Yes | String is an independent value |
Calling c.Next() in middleware | Yes | Continues the chain synchronously |
If you must perform background work, copy the data first:
// Unsafe: the Context pointer may be reused.
go func() {
log.Println(c.Path()) // data race
}()
// Safe: copy the primitive before the goroutine.
p := c.Path()
go func(path string) {
log.Println(path)
}(p)
Expected checks
- Race detection: run
go run -race main.goin the project directory. The race detector should report no races when you send requests to both versioned endpoints. - Header persistence: if your middleware sets a header with
c.Set("X-Request-ID", "abc"), sendcurl -i http://localhost:3000/v1/users/42and confirm the header appears in the response. - Parameter retrieval: the JSON body for
/v1/users/42should contain"id":"42"and"version":"v1". - Group isolation: requests to
/v2/users/42should return the v2 payload, proving the groups are distinct.
Limitations and recovery options
Direct fasthttp access. Fiber exposes the underlying fasthttp request and response through c.Request() and c.Response(). Bypassing Fiber’s methods can corrupt memory because fasthttp pools buffers aggressively. If you encounter instability after direct access, revert those calls to Fiber equivalents such as c.Get(), c.Set(), and c.Send().
net/http compatibility. Fiber is not a drop-in replacement for net/http handlers. If you need to integrate an existing net/http middleware, use a compatibility adapter rather than casting types. If the adapter is unavailable, wrap the legacy handler in a Fiber handler that copies the request body and headers.
Context lifetime. If a data race is detected in a goroutine, the recovery path is to refactor the code so that only copied values—strings, integers, or byte slices—cross the goroutine boundary. Do not attempt to lock or extend the lifetime of the pooled Context.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.