Using Fiber’s Ctx.Locals to Pass Auth Data Between Middleware and Handlers
Fiber’s Ctx.Locals lets you attach request‑scoped data, like a user record, without global state. This blog walks through an auth‑caching example, discusses type safety, and shows how to verify isolation and correctness.
27 Sept 2025, 17:54 UTC

Why Request‑Scoped Data Is a Pain Point
When building a REST API in Go, a common pattern is to validate a bearer token in a middleware, fetch the user record from a database, and then let the route handler perform business logic. If the handler also needs the user, it would normally re‑query the database or rely on a global cache, both of which add latency or complexity.
Fiber offers a built‑in, request‑scoped key–value store called Ctx.Locals. It lets you attach data to the current request context and read it later in the same lifecycle without leaking it to other requests.
The Ctx.Locals API in a Nutshell
// Set a value
c.Locals("user", userStruct)
// Read a value
if u, ok := c.Locals("user").(*User); ok {
// u is safe to use
}
Key points to remember:
- Mutable per‑request store – Fiber pools
*fiber.Ctxobjects, resetting the locals map on each request. - Values are stored as
interface{}, so you must perform a type assertion. - It is not a replacement for Go’s
context.Contextwhen you need to pass values to external libraries.
Auth‑Caching Example: From Token to User Record
Below is a minimal, self‑contained example that demonstrates how to cache the user record in Ctx.Locals after validating a JWT. The route handler can then retrieve the user without an additional DB call.
package main
import (
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
// User represents a simplified user model.
type User struct {
ID int
Email string
}
// fakeDB simulates a database lookup.
func fakeDB(userID int) (*User, error) {
// In real life, query your database here.
return &User{ID: userID, Email: fmt.Sprintf("[contact removed]", userID)}, nil
}
// authMiddleware validates the JWT and caches the user.
func authMiddleware(c *fiber.Ctx) error {
// 1. Extract token from Authorization header.
authHeader := c.Get("Authorization")
if authHeader == "" {
return c.Status(fiber.StatusUnauthorized).SendString("missing token")
}
tokenString := authHeader[len("Bearer "):]
// 2. Parse the token (replace "mysecret" with your key).
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
return []byte("mysecret"), nil
})
if err != nil || !token.Valid {
return c.Status(fiber.StatusUnauthorized).SendString("invalid token")
}
// 3. Extract user ID claim.
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return c.Status(fiber.StatusUnauthorized).SendString("bad claims")
}
uidFloat, ok := claims["sub"].(float64)
if !ok {
return c.Status(fiber.StatusUnauthorized).SendString("missing sub")
}
userID := int(uidFloat)
// 4. Lookup user once and cache in Locals.
user, err := fakeDB(userID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("db error")
}
c.Locals("user", user)
// Continue to next handler.
return c.Next()
}
// profileHandler reads the cached user and returns a greeting.
func profileHandler(c *fiber.Ctx) error {
u, ok := c.Locals("user").(*User)
if !ok || u == nil {
return c.Status(fiber.StatusInternalServerError).SendString("user not found in Locals")
}
return c.SendString(fmt.Sprintf("Hello, %s!", u.Email))
}
func main() {
app := fiber.New()
// Protect the /profile route with authMiddleware.
app.Get("/profile", authMiddleware, profileHandler)
app.Listen(":3000")
}
In this setup:
- The middleware validates the JWT and fetches the user once.
- It stores the
*UserinCtx.Locals. - The handler reads the same instance, avoiding a second DB round‑trip.
Type Safety Trade‑Offs and Mitigation
Because Ctx.Locals accepts interface{}, you must perform a type assertion. A mismatched assertion will panic. Use the comma‑ok idiom to guard against this:
if u, ok := c.Locals("user").(*User); ok {
// use u
} else {
// handle missing or wrong type
}
Other strategies:
- Wrap
Ctx.Localsin helper functions that perform the assertion and return a typed error. - Define a custom key type (e.g.,
type ctxKey string) to reduce accidental key collisions. - Prefer
context.Contextfor values that will be passed to libraries that expect a Go context.
Verification Checklist
- Confirm Locals Reset – Run concurrent requests against a test endpoint that writes a unique value to
Localsand reads it back. Verify that each request receives its own value. - Check Pooling Behavior – Inspect the Fiber source (look for
ctx.reset()) to see thatlocalsis cleared before returning the context to the sync.Pool. - Test Type Assertions – Intentionally store a different type (e.g.,
int) and ensure your handler handles theokcheck gracefully. - Run
go vetandgolangci-lintto catch any misuse ofCtx.Localsin your codebase.
Takeaway
Fiber’s Ctx.Locals is a lightweight, per‑request store that can dramatically reduce duplicate work, such as re‑fetching a user record for each route. The trade‑off is the need for explicit type assertions and the awareness that it is not a drop‑in replacement for context.Context in external libraries. By following the example and verification steps above, you can safely integrate Ctx.Locals into your authentication flow and keep your handlers clean and efficient.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.