Fiber #
Fiber is a Go web framework built on Fasthttp — the fastest HTTP engine for Go — and inspired by Express.js. The result is a framework whose throughput consistently beats both Gin and Echo across various benchmarks, while offering an API familiar to developers who’ve worked with Node.js. Fiber is a good fit for building high-throughput services such as API gateways, proxies, or endpoints that must handle hundreds of thousands of requests per second. This article covers all of Fiber’s main features: routing, middleware, request parsing, responses, WebSockets, and code organization patterns for production.
Installation #
go get github.com/gofiber/fiber/v2
A minimal server to verify the installation:
package main
import "github.com/gofiber/fiber/v2"
func main() {
app := fiber.New()
app.Get("/ping", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "pong"})
})
app.Listen(":8080")
}
Unlike Gin, which uses the standardnet/http, Fiber uses Fasthttp, which is not compatible withhttp.Handler. This means middleware or libraries written fornet/httpcan’t be used directly in Fiber. Fiber provides theadaptorpackage for these cases.
How Requests Work in Fiber #
Understanding Fiber’s architecture is important before writing code. Fiber leverages Fasthttp’s zero-allocation request handling — meaning *fiber.Ctx objects are reused between requests to avoid garbage collector pressure.
Engine Differences: net/http vs fasthttp #
Here are the fundamental differences between the standard Go engine (net/http), used by Gin/Echo, and the fasthttp engine underlying Fiber:
| Characteristic | net/http (Gin, Echo) | fasthttp (Fiber) |
|---|---|---|
| Memory Allocation | Allocates new objects per request (more garbage collector pressure) | Uses a memory pool and reuses context objects (Zero-Allocation) |
| HTTP Compliance | Supports HTTP/1.x, HTTP/2, and broad standard compatibility | Very fast, but doesn’t support all HTTP protocol features completely |
| Ecosystem Compatibility | Fully compatible with the entire Go third-party library ecosystem | Needs a special adapter to use net/http middleware |
| Load Optimization | Very stable for all workload types | Excels at high throughput with small-to-medium payloads |
flowchart TD
A([HTTP Request]) --> B[Fasthttp Engine]
B --> C[fiber.Ctx from the pool]
C --> D[Router — trie-based matching]
D --> E{Route found?}
E -- No --> F[404 / Error Handler]
E -- Yes --> G[Middleware Chain]
G --> H[Main Handler]
H --> I[Response written to Fasthttp]
I --> J[fiber.Ctx returned to the pool]
J --> K([Response to the Client])
F --> KBecause*fiber.Ctxis reused from a pool, don’t store references tocin other goroutines. If you need to use context data in a separate goroutine, copy the values first before spawning the goroutine.
App Configuration #
Fiber accepts a fiber.Config at initialization to customize server behavior:
app := fiber.New(fiber.Config{
AppName: "MyAPI v1.0",
Prefork: false, // true: fork per CPU core (Linux only)
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
BodyLimit: 4 * 1024 * 1024, // 4 MB body limit
// Custom global error handler
ErrorHandler: func(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
var e *fiber.Error
if errors.As(err, &e) {
code = e.Code
}
return c.Status(code).JSON(fiber.Map{
"success": false,
"error": err.Error(),
})
},
})
Production Mode #
// ANTI-PATTERN: error stack traces exposed to the client in production
app := fiber.New() // default PrintRoutes: true, stack trace: true
// CORRECT: disable sensitive info in production
isProd := os.Getenv("APP_ENV") == "production"
app := fiber.New(fiber.Config{
AppName: "MyAPI",
DisableStartupMessage: isProd,
// Use a custom ErrorHandler so stack traces don't leak
ErrorHandler: productionErrorHandler,
})
Routing #
Routing in Fiber uses the HTTP method as the function name. The syntax is intentionally similar to Express.js.
app.Get("/users", listUsers)
app.Post("/users", createUser)
app.Put("/users/:id", updateUser)
app.Patch("/users/:id", patchUser)
app.Delete("/users/:id", deleteUser)
// All methods
app.All("/webhook", handleWebhook)
Route Parameters #
// Required parameter
app.Get("/users/:id", func(c *fiber.Ctx) error {
id := c.Params("id")
return c.JSON(fiber.Map{"id": id})
})
// Optional parameter — matches /posts and /posts/123
app.Get("/posts/:id?", func(c *fiber.Ctx) error {
id := c.Params("id", "all") // "all" if absent
return c.JSON(fiber.Map{"id": id})
})
// Wildcard
app.Get("/files/*", func(c *fiber.Ctx) error {
path := c.Params("*")
return c.JSON(fiber.Map{"path": path})
})
Query Strings and Headers #
// GET /search?q=golang&page=2
app.Get("/search", func(c *fiber.Ctx) error {
q := c.Query("q")
page := c.QueryInt("page", 1) // default 1 if absent
return c.JSON(fiber.Map{"q": q, "page": page})
})
// Reading headers
app.Get("/profile", func(c *fiber.Ctx) error {
token := c.Get("Authorization")
lang := c.Get("Accept-Language", "id") // default "id"
return c.JSON(fiber.Map{"token": token, "lang": lang})
})
Route Groups #
api := app.Group("/api")
// v1 — no auth
v1 := api.Group("/v1")
v1.Get("/health", healthHandler)
// v2 — with auth middleware
v2 := api.Group("/v2", authMiddleware)
v2.Get("/users", listUsers)
v2.Post("/users", createUser)
// Nested groups
admin := v2.Group("/admin", adminOnlyMiddleware)
admin.Get("/metrics", metricsHandler)
admin.Delete("/users/:id", forceDeleteUser)
graph TD
A["/api"] --> B["/v1"]
A --> C["/v2 + authMiddleware"]
B --> D["GET /health"]
C --> E["GET /users"]
C --> F["POST /users"]
C --> G["/admin + adminOnly"]
G --> H["GET /metrics"]
G --> I["DELETE /users/:id"]Middleware #
Middleware in Fiber has the type fiber.Handler — the same as a regular handler. Fiber provides many official middlewares via separate packages under gofiber/fiber/v2/middleware.
Built-in Middleware #
import (
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/compress"
)
app := fiber.New()
// Recovery — catches panics
app.Use(recover.New())
// Logger
app.Use(logger.New(logger.Config{
Format: "${time} | ${status} | ${latency} | ${method} ${path}\n",
}))
// CORS
app.Use(cors.New(cors.Config{
AllowOrigins: "https://app.example.com",
AllowHeaders: "Origin, Content-Type, Authorization",
AllowMethods: "GET, POST, PUT, DELETE",
}))
// Rate limiter
app.Use(limiter.New(limiter.Config{
Max: 100, // 100 requests
Expiration: 1 * time.Minute, // per minute per IP
}))
// Response compression
app.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed,
}))
Custom Middleware #
func RequestIDMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
requestID := uuid.New().String()
// Store in locals — similar to c.Set in Gin
c.Locals("requestID", requestID)
// Add to the response header
c.Set("X-Request-ID", requestID)
return c.Next() // continue to the next handler
}
}
func AuthMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
return fiber.NewError(fiber.StatusUnauthorized, "token not found")
}
userID, err := validateToken(token)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "invalid token")
}
c.Locals("userID", userID)
return c.Next()
}
}
Middleware Execution Order #
sequenceDiagram
participant Client
participant RID as RequestIDMiddleware
participant Auth as AuthMiddleware
participant H as Handler
Client->>RID: Request enters
RID->>RID: Generate a request ID
RID->>Auth: c.Next()
Auth->>Auth: Validate the token
Auth->>H: c.Next()
H->>H: Business logic
H-->>Auth: return response
Auth-->>RID: return
RID->>RID: Set the X-Request-ID header
RID-->>Client: Response + X-Request-ID headerRequest Parsing #
Fiber provides the BodyParser method to decode the request body into a struct, with automatic support for JSON, XML, and forms based on the Content-Type.
Body Parsing #
type CreateProductRequest struct {
Name string `json:"name" form:"name" xml:"name" validate:"required,min=3"`
Price float64 `json:"price" form:"price" xml:"price" validate:"required,gt=0"`
Stock int `json:"stock" form:"stock" xml:"stock" validate:"required,gte=0"`
Category string `json:"category" form:"category" xml:"category" validate:"required"`
}
func createProduct(c *fiber.Ctx) error {
var req CreateProductRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid request body")
}
// Fiber doesn't have a built-in validator — use go-playground/validator
if err := validate.Struct(req); err != nil {
return fiber.NewError(fiber.StatusUnprocessableEntity, err.Error())
}
// business logic...
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"success": true,
"data": req,
})
}
Unlike Gin, which hasbindingtags integrated with validation, Fiber doesn’t include a built-in validator. You need to initializego-playground/validatormanually and call it afterBodyParser. This gives more flexibility, but requires additional boilerplate.
Validator Integration #
import "github.com/go-playground/validator/v10"
// Initialize once, reuse (validator is thread-safe)
var validate = validator.New()
// Helper for more informative errors
func parseValidationErrors(err error) []fiber.Map {
var errs []fiber.Map
for _, e := range err.(validator.ValidationErrors) {
errs = append(errs, fiber.Map{
"field": e.Field(),
"message": validationMessage(e),
})
}
return errs
}
func validationMessage(e validator.FieldError) string {
switch e.Tag() {
case "required":
return e.Field() + " is required"
case "min":
return e.Field() + " must be at least " + e.Param() + " characters"
case "gt":
return e.Field() + " must be greater than " + e.Param()
default:
return e.Field() + " is invalid"
}
}
Parsing Query and Params into Structs #
type PaginationQuery struct {
Page int `query:"page"`
Limit int `query:"limit"`
Sort string `query:"sort"`
}
func listUsers(c *fiber.Ctx) error {
var q PaginationQuery
if err := c.QueryParser(&q); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
// Default values
if q.Page <= 0 {
q.Page = 1
}
if q.Limit <= 0 || q.Limit > 100 {
q.Limit = 20
}
// process...
return c.JSON(fiber.Map{"page": q.Page, "limit": q.Limit})
}
flowchart LR
A["JSON Body\nContent-Type: application/json"] --> E
B["Form Data\nContent-Type: multipart/form-data"] --> E
C["XML Body\nContent-Type: application/xml"] --> E
D["Query String\n?page=1&limit=20"] --> F["QueryParser"]
E["BodyParser"] --> G[Go Struct]
F --> G
G --> H{go-playground\nvalidator}
H -- Failed --> I["422 / 400"]
H -- Passed --> J[Handler Logic]Responses #
Fiber provides a rich, expressive response API. These methods can be chained because they all return an error.
JSON and Other Formats #
// JSON
return c.JSON(fiber.Map{"success": true, "data": user})
// JSON with an explicit status code
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"success": true,
"data": user,
})
// String
return c.SendString("Hello, World!")
// Status without a body (DELETE)
return c.SendStatus(fiber.StatusNoContent)
// XML
return c.XML(user)
// File download
return c.Download("/path/to/file.pdf", "report.pdf")
// Redirect
return c.Redirect("https://example.com", fiber.StatusMovedPermanently)
Centralized Response Helpers #
Just like with Gin, having a consistent response helper is highly recommended:
// pkg/response/response.go
package response
import "github.com/gofiber/fiber/v2"
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Meta interface{} `json:"meta,omitempty"`
}
type PaginatedMeta struct {
Page int `json:"page"`
Limit int `json:"limit"`
TotalItems int `json:"total_items"`
TotalPages int `json:"total_pages"`
}
func OK(c *fiber.Ctx, data interface{}) error {
return c.JSON(Response{Success: true, Data: data})
}
func Created(c *fiber.Ctx, data interface{}) error {
return c.Status(fiber.StatusCreated).JSON(Response{
Success: true, Data: data,
})
}
func Paginated(c *fiber.Ctx, data interface{}, meta PaginatedMeta) error {
return c.JSON(Response{Success: true, Data: data, Meta: meta})
}
func BadRequest(c *fiber.Ctx, msg string) error {
return c.Status(fiber.StatusBadRequest).JSON(Response{
Success: false, Error: msg,
})
}
func NotFound(c *fiber.Ctx, resource string) error {
return c.Status(fiber.StatusNotFound).JSON(Response{
Success: false, Error: resource + " not found",
})
}
func InternalError(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(Response{
Success: false, Error: "an internal error occurred",
})
}
File Uploads #
// Single file upload
app.Post("/upload", func(c *fiber.Ctx) error {
file, err := c.FormFile("file")
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "file not found")
}
// Validate the size (e.g. max 5 MB)
if file.Size > 5*1024*1024 {
return fiber.NewError(fiber.StatusRequestEntityTooLarge,
"file size exceeds the 5 MB limit")
}
// Validate the extension
ext := filepath.Ext(file.Filename)
allowed := map[string]bool{".jpg": true, ".png": true, ".pdf": true}
if !allowed[strings.ToLower(ext)] {
return fiber.NewError(fiber.StatusBadRequest, "file type not allowed")
}
// Save with a unique name
filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
dst := filepath.Join("uploads", filename)
if err := c.SaveFile(file, dst); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to save the file")
}
return c.JSON(fiber.Map{
"filename": filename,
"size": file.Size,
})
})
WebSockets #
One of Fiber’s advantages over Gin is more mature WebSocket support via the gofiber/websocket package.
go get github.com/gofiber/websocket/v2
import "github.com/gofiber/websocket/v2"
// Middleware to upgrade the connection
app.Use("/ws", func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) {
c.Locals("allowed", true)
return c.Next()
}
return fiber.ErrUpgradeRequired
})
// WebSocket handler
app.Get("/ws/chat", websocket.New(func(c *websocket.Conn) {
// c.Locals is available from the middleware
userID := c.Locals("userID")
for {
msgType, msg, err := c.ReadMessage()
if err != nil {
// Client disconnected
break
}
// Echo the message back
if err := c.WriteMessage(msgType, msg); err != nil {
break
}
}
}, websocket.Config{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}))
sequenceDiagram
participant Client
participant MW as WebSocket Middleware
participant WS as WebSocket Handler
Client->>MW: HTTP GET /ws/chat\nUpgrade: websocket
MW->>MW: IsWebSocketUpgrade?
MW->>WS: c.Next()
WS->>Client: 101 Switching Protocols
loop Active connection
Client->>WS: WriteMessage(data)
WS->>WS: Process the message
WS->>Client: WriteMessage(response)
end
Client->>WS: Disconnect
WS->>WS: Loop breaks, cleanupLocals and Data Sharing #
Fiber uses c.Locals() to share data between middlewares and handlers — equivalent to c.Set()/c.Get() in Gin.
// In a middleware — store data
c.Locals("userID", 42)
c.Locals("role", "admin")
// In a handler — read data
userID := c.Locals("userID").(int)
role := c.Locals("role").(string)
Typed Locals Helpers #
// ANTI-PATTERN: direct type assertion, panics if nil
func getUser(c *fiber.Ctx) error {
userID := c.Locals("userID").(int) // panics if the middleware didn't run
// ...
}
// CORRECT: a helper with nil handling
func GetUserID(c *fiber.Ctx) (int, bool) {
val := c.Locals("userID")
if val == nil {
return 0, false
}
id, ok := val.(int)
return id, ok
}
func getUser(c *fiber.Ctx) error {
userID, ok := GetUserID(c)
if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "not authenticated")
}
// use userID
return c.JSON(fiber.Map{"userID": userID})
}
Error Handling #
Fiber has an elegant global error handling system. Return an error from the handler and let the ErrorHandler deal with it.
// Use fiber.NewError for errors with status codes
app.Get("/users/:id", func(c *fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "ID must be a number")
}
user, err := userService.GetByID(id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return fiber.NewError(fiber.StatusNotFound, "user not found")
}
// Unexpected error — return without details to the client
log.Printf("GetByID error: %v", err)
return fiber.ErrInternalServerError
}
return c.JSON(user)
})
Custom Global Error Handler #
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
message := "an internal error occurred"
var e *fiber.Error
if errors.As(err, &e) {
code = e.Code
message = e.Message
}
return c.Status(code).JSON(fiber.Map{
"success": false,
"error": message,
"path": c.Path(),
})
},
})
flowchart TD
A[Handler returns an error] --> B{errors.As fiber.Error?}
B -- Yes --> C[Take Code and Message from fiber.Error]
B -- No --> D[Code: 500, Message: internal error]
C --> E[c.Status code .JSON response]
D --> E
E --> F([Response to the Client])Recommended Project Structure #
myapp/
├── main.go
├── internal/
│ ├── handler/
│ │ ├── user.go
│ │ └── product.go
│ ├── middleware/
│ │ ├── auth.go
│ │ └── request_id.go
│ ├── service/
│ │ ├── user.go
│ │ └── product.go
│ └── repository/
│ ├── user.go
│ └── product.go
├── pkg/
│ ├── response/
│ │ └── response.go
│ └── validator/
│ └── validator.go
└── router/
└── router.go
Router Initialization #
// router/router.go
package router
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/logger"
"myapp/internal/handler"
"myapp/internal/middleware"
)
func Setup(userHandler *handler.UserHandler) *fiber.App {
app := fiber.New(fiber.Config{
ErrorHandler: middleware.ErrorHandler,
})
// Global middleware
app.Use(recover.New())
app.Use(logger.New())
app.Use(middleware.RequestID())
// Health check
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "ok"})
})
// API routes
api := app.Group("/api/v1")
api.Use(middleware.Auth())
users := api.Group("/users")
users.Get("", userHandler.List)
users.Post("", userHandler.Create)
users.Get("/:id", userHandler.GetByID)
users.Put("/:id", userHandler.Update)
users.Delete("/:id", userHandler.Delete)
return app
}
Graceful Shutdown #
Fiber supports graceful shutdown — ensuring all in-flight requests finish before the server stops.
func main() {
app := fiber.New()
// ... set up routes
// Run the server in a separate goroutine
go func() {
if err := app.Listen(":8080"); err != nil {
log.Printf("server error: %v", err)
}
}()
// Wait for an OS signal (Ctrl+C or SIGTERM from the orchestrator)
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
log.Println("Shutting down the server...")
// Give 10 seconds for in-flight requests to finish
if err := app.ShutdownWithTimeout(10 * time.Second); err != nil {
log.Printf("shutdown error: %v", err)
}
log.Println("Server stopped.")
}
When Not to Use Fiber #
Keep using Fiber if:
✓ Extreme performance is the top priority
✓ The team is familiar with Express.js and wants to transition to Go
✓ You need mature built-in WebSocket support
✓ You're building a proxy, gateway, or high-throughput service
Consider Gin if:
✗ You need full compatibility with the net/http ecosystem
✗ The team is more familiar with established Gin conventions
✗ The third-party libraries you use expect an http.Handler
Consider Echo if:
✗ You need native HTTP/2 or more flexible binding
✗ You want a framework closer to Go standards
Consider the standard net/http if:
✗ The application is very simple and framework overhead isn't desired
Summary #
- Fasthttp, not net/http — Fiber is built on Fasthttp, which is incompatible with
http.Handler; use theadaptorpackage if you need net/http middleware.*fiber.Ctxis reused — don’t store context references in other goroutines; copy the values you need first.BodyParserauto-detects the format — JSON, XML, and forms are handled automatically based on theContent-Type, without different methods per format.- No built-in validator — add
go-playground/validatormanually and create informative error helpers.fiber.NewErrorfor structured errors — return errors with the right status code and let the globalErrorHandlerformat them.c.Locals()for data sharing — use typed helpers to avoid type assertions that could panic.- Official middleware is available separately — logger, recover, CORS, rate limiter, compress, and more are available in the
middlewaresubpackages.- Graceful shutdown — always implement
ShutdownWithTimeoutin production so in-flight requests aren’t forcibly interrupted.