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 standard net/http, Fiber uses Fasthttp, which is not compatible with http.Handler. This means middleware or libraries written for net/http can’t be used directly in Fiber. Fiber provides the adaptor package 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:

Characteristicnet/http (Gin, Echo)fasthttp (Fiber)
Memory AllocationAllocates new objects per request (more garbage collector pressure)Uses a memory pool and reuses context objects (Zero-Allocation)
HTTP ComplianceSupports HTTP/1.x, HTTP/2, and broad standard compatibilityVery fast, but doesn’t support all HTTP protocol features completely
Ecosystem CompatibilityFully compatible with the entire Go third-party library ecosystemNeeds a special adapter to use net/http middleware
Load OptimizationVery stable for all workload typesExcels 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 --> K
Because *fiber.Ctx is reused from a pool, don’t store references to c in 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 header

Request 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 has binding tags integrated with validation, Fiber doesn’t include a built-in validator. You need to initialize go-playground/validator manually and call it after BodyParser. 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, cleanup

Locals 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])

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 the adaptor package if you need net/http middleware.
  • *fiber.Ctx is reused — don’t store context references in other goroutines; copy the values you need first.
  • BodyParser auto-detects the format — JSON, XML, and forms are handled automatically based on the Content-Type, without different methods per format.
  • No built-in validator — add go-playground/validator manually and create informative error helpers.
  • fiber.NewError for structured errors — return errors with the right status code and let the global ErrorHandler format 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 middleware subpackages.
  • Graceful shutdown — always implement ShutdownWithTimeout in production so in-flight requests aren’t forcibly interrupted.

← Previous: Gin   Next: Echo →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact