Back to Posts
Backend #Golang #Fiber #API

Building REST APIs with Golang Fiber

A comprehensive guide to building high-performance REST APIs using the Fiber web framework in Go.

8 min read
Building REST APIs with Golang Fiber

Building REST APIs with Golang Fiber

Introduction: In the modern web development landscape, speed and efficiency are paramount. While Go is already known for its excellent performance, choosing the right web framework can make a significant difference in your API's responsiveness and scalability. Enter Fiber—a web framework that combines the familiar syntax of Express.js with the raw speed of Go's Fasthttp engine. Whether you're building microservices, RESTful APIs, or high-throughput web applications, Fiber provides the tools and performance you need to deliver exceptional results.

Fiber is an Express-inspired web framework built on top of Fasthttp, the fastest HTTP engine for Go. It's designed to ease things up for fast development with zero memory allocation and performance in mind. With its intuitive API, extensive middleware ecosystem, and impressive benchmarks, Fiber has become one of the most popular choices for building web applications in Go.


Table of Contents

  1. Why Choose Fiber?
  2. Installation and Setup
  3. Your First Fiber Application
  4. Routing Fundamentals
  5. Request Handling
  6. Response Methods
  7. Middleware
  8. Building a Complete REST API
  9. Request Validation
  10. Database Integration
  11. Error Handling
  12. Authentication and Authorization
  13. File Upload and Static Files
  14. Testing Your API
  15. Deployment Best Practices

Why Choose Fiber?

Before we dive into code, let's understand what makes Fiber an excellent choice for building REST APIs.

Performance

Fiber is built on top of Fasthttp, which is significantly faster than Go's standard net/http package. In benchmarks, Fiber consistently outperforms other Go web frameworks, handling over 6 million requests per second on a single machine. This performance makes it ideal for high-traffic applications and microservices.

Express-Like Syntax

If you've worked with Express.js in Node.js, Fiber will feel immediately familiar. This familiarity reduces the learning curve and makes it easy to translate concepts between ecosystems. You get the developer experience of Express with the performance of Go.

Zero Memory Allocation

Fiber is designed to minimize memory allocations, reducing garbage collection overhead and improving overall performance. This design philosophy makes it particularly well-suited for building efficient, scalable services.

Rich Middleware Ecosystem

Fiber comes with a comprehensive set of built-in middleware for common tasks like CORS, logging, compression, rate limiting, and more. The middleware system is easy to use and extend, allowing you to build complex functionality with minimal code.

Robust Routing

Fiber provides a powerful routing system with support for route parameters, query strings, wildcards, and route grouping. This makes it easy to organize your API endpoints logically and maintain clean code.

Active Community

With over 30,000 stars on GitHub and an active community, Fiber has excellent documentation, numerous examples, and a wealth of community-contributed middleware and extensions.


Installation and Setup

Let's set up a new Go project with Fiber.

Prerequisites

Ensure you have Go installed (version 1.17 or higher):

go version

Create a New Project

# Create project directory
mkdir fiber-api
cd fiber-api

# Initialize Go module
go mod init github.com/yourusername/fiber-api

# Install Fiber
go get -u github.com/gofiber/fiber/v2

Project Structure

For a well-organized API, use this structure:

fiber-api/
├── main.go
├── go.mod
├── go.sum
├── handlers/
│   ├── user.go
│   └── auth.go
├── models/
│   └── user.go
├── middleware/
│   └── auth.go
├── database/
│   └── connection.go
├── routes/
│   └── routes.go
└── utils/
    └── helpers.go

Your First Fiber Application

Let's create a simple "Hello World" application to understand Fiber's basics.

Basic Server

Create main.go:

package main

import (
    "log"
    "github.com/gofiber/fiber/v2"
)

func main() {
    // Create a new Fiber instance
    app := fiber.New()

    // Define a route
    app.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("Hello, World!")
    })

    // Start server on port 3000
    log.Fatal(app.Listen(":3000"))
}

Run the application:

go run main.go

Visit http://localhost:3000 to see your API in action.

Custom Configuration

Fiber allows extensive configuration:

app := fiber.New(fiber.Config{
    ServerHeader: "Fiber",
    AppName:      "My API v1.0.0",
    Prefork:      false,
    StrictRouting: false,
    CaseSensitive: false,
    ErrorHandler: customErrorHandler,
    ReadTimeout:  time.Second * 10,
    WriteTimeout: time.Second * 10,
})

Graceful Shutdown

Implement graceful shutdown to handle ongoing requests:

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"
    
    "github.com/gofiber/fiber/v2"
)

func main() {
    app := fiber.New()

    app.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("Hello, World!")
    })

    // Channel to listen for interrupt signals
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)

    // Start server in a goroutine
    go func() {
        if err := app.Listen(":3000"); err != nil {
            log.Panic(err)
        }
    }()

    // Wait for interrupt signal
    <-c
    log.Println("Gracefully shutting down...")
    _ = app.Shutdown()

    log.Println("Server shutdown complete")
}

Routing Fundamentals

Fiber provides a powerful and intuitive routing system.

HTTP Methods

// GET request
app.Get("/users", getUsers)

// POST request
app.Post("/users", createUser)

// PUT request
app.Put("/users/:id", updateUser)

// DELETE request
app.Delete("/users/:id", deleteUser)

// PATCH request
app.Patch("/users/:id", patchUser)

// Handle all methods
app.All("/api", func(c *fiber.Ctx) error {
    return c.SendString("Any HTTP method")
})

Route Parameters

// Single parameter
app.Get("/users/:id", func(c *fiber.Ctx) error {
    id := c.Params("id")
    return c.SendString("User ID: " + id)
})

// Multiple parameters
app.Get("/users/:id/posts/:postId", func(c *fiber.Ctx) error {
    userID := c.Params("id")
    postID := c.Params("postId")
    return c.JSON(fiber.Map{
        "userID": userID,
        "postID": postID,
    })
})

// Optional parameters
app.Get("/users/:id?", func(c *fiber.Ctx) error {
    id := c.Params("id")
    if id == "" {
        return c.SendString("All users")
    }
    return c.SendString("User ID: " + id)
})

Wildcards

// Match anything after /api/
app.Get("/api/*", func(c *fiber.Ctx) error {
    path := c.Params("*")
    return c.SendString("Path: " + path)
})

// File serving
app.Get("/files/*", func(c *fiber.Ctx) error {
    return c.SendFile("./public/" + c.Params("*"))
})

Route Groups

Organize related routes together:

func setupRoutes(app *fiber.App) {
    // API v1 group
    v1 := app.Group("/api/v1")
    
    // Users routes
    users := v1.Group("/users")
    users.Get("/", getUsers)
    users.Get("/:id", getUser)
    users.Post("/", createUser)
    users.Put("/:id", updateUser)
    users.Delete("/:id", deleteUser)
    
    // Posts routes
    posts := v1.Group("/posts")
    posts.Get("/", getPosts)
    posts.Post("/", createPost)
    
    // API v2 group
    v2 := app.Group("/api/v2")
    v2.Get("/users", getUsersV2)
}

Route Naming and URLs

// Named routes
app.Get("/users/:id", getUser).Name("user")

// Generate URL from route name
url, err := app.GetRoute("user").BuildPath(map[string]string{
    "id": "123",
})
// url: "/users/123"

Request Handling

Fiber provides comprehensive methods to access request data.

Query Parameters

app.Get("/search", func(c *fiber.Ctx) error {
    // Single query parameter
    query := c.Query("q")
    
    // Query with default value
    page := c.Query("page", "1")
    
    // All query parameters
    queries := c.Queries()
    
    return c.JSON(fiber.Map{
        "query":   query,
        "page":    page,
        "all":     queries,
    })
})

// GET /search?q=fiber&page=2&sort=desc

Request Body

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

app.Post("/users", func(c *fiber.Ctx) error {
    user := new(User)
    
    // Parse JSON body
    if err := c.BodyParser(user); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Invalid request body",
        })
    }
    
    return c.JSON(user)
})

Headers

app.Get("/headers", func(c *fiber.Ctx) error {
    // Get single header
    auth := c.Get("Authorization")
    
    // Get header with default
    contentType := c.Get("Content-Type", "application/json")
    
    // Get all headers
    headers := c.GetReqHeaders()
    
    return c.JSON(fiber.Map{
        "authorization": auth,
        "contentType":   contentType,
        "all":          headers,
    })
})

Cookies

app.Get("/cookie", func(c *fiber.Ctx) error {
    // Get cookie
    value := c.Cookies("name")
    
    // Get cookie with default
    session := c.Cookies("session", "default-session")
    
    return c.SendString("Cookie value: " + value)
})

app.Post("/cookie", func(c *fiber.Ctx) error {
    // Set cookie
    c.Cookie(&fiber.Cookie{
        Name:     "session",
        Value:    "session-value",
        Expires:  time.Now().Add(24 * time.Hour),
        HTTPOnly: true,
        Secure:   true,
    })
    
    return c.SendString("Cookie set")
})

Form Data

app.Post("/form", func(c *fiber.Ctx) error {
    // Get form value
    name := c.FormValue("name")
    
    // Get form value with default
    email := c.FormValue("email", "default@example.com")
    
    // Get all form values
    data := c.Request().PostArgs()
    
    return c.JSON(fiber.Map{
        "name":  name,
        "email": email,
    })
})

Response Methods

Fiber provides multiple ways to send responses.

Sending Text

app.Get("/text", func(c *fiber.Ctx) error {
    return c.SendString("Plain text response")
})

Sending JSON

app.Get("/json", func(c *fiber.Ctx) error {
    return c.JSON(fiber.Map{
        "message": "Success",
        "data": fiber.Map{
            "id":   1,
            "name": "John Doe",
        },
    })
})

// With struct
type Response struct {
    Message string `json:"message"`
    Data    User   `json:"data"`
}

app.Get("/user", func(c *fiber.Ctx) error {
    return c.JSON(Response{
        Message: "Success",
        Data:    User{Name: "John", Email: "john@example.com"},
    })
})

Status Codes

// Set status and send
app.Get("/created", func(c *fiber.Ctx) error {
    return c.Status(201).JSON(fiber.Map{
        "message": "Created",
    })
})

// Common status codes
c.Status(fiber.StatusOK)                    // 200
c.Status(fiber.StatusCreated)               // 201
c.Status(fiber.StatusBadRequest)            // 400
c.Status(fiber.StatusUnauthorized)          // 401
c.Status(fiber.StatusNotFound)              // 404
c.Status(fiber.StatusInternalServerError)   // 500

Redirects

app.Get("/old", func(c *fiber.Ctx) error {
    return c.Redirect("/new")
})

// Redirect with status code
app.Get("/moved", func(c *fiber.Ctx) error {
    return c.Redirect("/new-location", 301)
})

Sending Files

app.Get("/download", func(c *fiber.Ctx) error {
    return c.Download("./files/document.pdf")
})

app.Get("/file", func(c *fiber.Ctx) error {
    return c.SendFile("./public/image.png")
})

Custom Headers

app.Get("/headers", func(c *fiber.Ctx) error {
    c.Set("X-Custom-Header", "value")
    c.Set("Cache-Control", "no-cache")
    
    return c.SendString("Headers set")
})

Middleware

Middleware functions execute before or after route handlers, enabling powerful functionality.

Built-in Middleware

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/gofiber/fiber/v2/middleware/compress"
    "github.com/gofiber/fiber/v2/middleware/recover"
)

func main() {
    app := fiber.New()
    
    // Logger middleware
    app.Use(logger.New(logger.Config{
        Format: "[${time}] ${status} - ${method} ${path}\n",
    }))
    
    // CORS middleware
    app.Use(cors.New(cors.Config{
        AllowOrigins: "https://example.com",
        AllowHeaders: "Origin, Content-Type, Accept",
    }))
    
    // Compress middleware
    app.Use(compress.New())
    
    // Recover middleware (panic recovery)
    app.Use(recover.New())
    
    // Routes...
}

Custom Middleware

// Simple middleware
func authMiddleware(c *fiber.Ctx) error {
    token := c.Get("Authorization")
    
    if token == "" {
        return c.Status(401).JSON(fiber.Map{
            "error": "Unauthorized",
        })
    }
    
    // Continue to next handler
    return c.Next()
}

// Use middleware
app.Use(authMiddleware)

// Or for specific routes
app.Get("/protected", authMiddleware, protectedHandler)

Middleware with Context

func userMiddleware(c *fiber.Ctx) error {
    // Get user from database or token
    user := getUserFromToken(c.Get("Authorization"))
    
    // Store in context
    c.Locals("user", user)
    
    return c.Next()
}

func protectedHandler(c *fiber.Ctx) error {
    // Retrieve from context
    user := c.Locals("user").(User)
    
    return c.JSON(fiber.Map{
        "user": user,
    })
}

Route-Specific Middleware

// Apply to specific routes
app.Get("/admin", adminMiddleware, adminHandler)

// Apply to route group
admin := app.Group("/admin", adminMiddleware)
admin.Get("/users", getUsers)
admin.Get("/settings", getSettings)

Building a Complete REST API

Let's build a complete CRUD API for managing users.

Define Models

// models/user.go
package models

type User struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    Age       int    `json:"age"`
    CreatedAt string `json:"created_at"`
}

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required,min=3"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"required,min=18,max=120"`
}

type UpdateUserRequest struct {
    Name  string `json:"name,omitempty"`
    Email string `json:"email,omitempty"`
    Age   int    `json:"age,omitempty"`
}

Create Handlers

// handlers/user.go
package handlers

import (
    "github.com/gofiber/fiber/v2"
    "github.com/yourusername/fiber-api/models"
)

// In-memory storage (use database in production)
var users = []models.User{
    {ID: 1, Name: "John Doe", Email: "john@example.com", Age: 30},
    {ID: 2, Name: "Jane Smith", Email: "jane@example.com", Age: 25},
}
var nextID = 3

// GetUsers retrieves all users
func GetUsers(c *fiber.Ctx) error {
    return c.JSON(fiber.Map{
        "success": true,
        "data":    users,
    })
}

// GetUser retrieves a single user by ID
func GetUser(c *fiber.Ctx) error {
    id, err := c.ParamsInt("id")
    if err != nil {
        return c.Status(400).JSON(fiber.Map{
            "success": false,
            "error":   "Invalid user ID",
        })
    }
    
    for _, user := range users {
        if user.ID == id {
            return c.JSON(fiber.Map{
                "success": true,
                "data":    user,
            })
        }
    }
    
    return c.Status(404).JSON(fiber.Map{
        "success": false,
        "error":   "User not found",
    })
}

// CreateUser creates a new user
func CreateUser(c *fiber.Ctx) error {
    req := new(models.CreateUserRequest)
    
    if err := c.BodyParser(req); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "success": false,
            "error":   "Invalid request body",
        })
    }
    
    // Validate request (implement validation)
    if req.Name == "" || req.Email == "" {
        return c.Status(400).JSON(fiber.Map{
            "success": false,
            "error":   "Name and email are required",
        })
    }
    
    user := models.User{
        ID:    nextID,
        Name:  req.Name,
        Email: req.Email,
        Age:   req.Age,
    }
    nextID++
    
    users = append(users, user)
    
    return c.Status(201).JSON(fiber.Map{
        "success": true,
        "data":    user,
    })
}

// UpdateUser updates an existing user
func UpdateUser(c *fiber.Ctx) error {
    id, err := c.ParamsInt("id")
    if err != nil {
        return c.Status(400).JSON(fiber.Map{
            "success": false,
            "error":   "Invalid user ID",
        })
    }
    
    req := new(models.UpdateUserRequest)
    if err := c.BodyParser(req); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "success": false,
            "error":   "Invalid request body",
        })
    }
    
    for i, user := range users {
        if user.ID == id {
            if req.Name != "" {
                users[i].Name = req.Name
            }
            if req.Email != "" {
                users[i].Email = req.Email
            }
            if req.Age != 0 {
                users[i].Age = req.Age
            }
            
            return c.JSON(fiber.Map{
                "success": true,
                "data":    users[i],
            })
        }
    }
    
    return c.Status(404).JSON(fiber.Map{
        "success": false,
        "error":   "User not found",
    })
}

// DeleteUser deletes a user
func DeleteUser(c *fiber.Ctx) error {
    id, err := c.ParamsInt("id")
    if err != nil {
        return c.Status(400).JSON(fiber.Map{
            "success": false,
            "error":   "Invalid user ID",
        })
    }
    
    for i, user := range users {
        if user.ID == id {
            users = append(users[:i], users[i+1:]...)
            
            return c.JSON(fiber.Map{
                "success": true,
                "message": "User deleted successfully",
            })
        }
    }
    
    return c.Status(404).JSON(fiber.Map{
        "success": false,
        "error":   "User not found",
    })
}

Setup Routes

// routes/routes.go
package routes

import (
    "github.com/gofiber/fiber/v2"
    "github.com/yourusername/fiber-api/handlers"
)

func Setup(app *fiber.App) {
    api := app.Group("/api/v1")
    
    // User routes
    users := api.Group("/users")
    users.Get("/", handlers.GetUsers)
    users.Get("/:id", handlers.GetUser)
    users.Post("/", handlers.CreateUser)
    users.Put("/:id", handlers.UpdateUser)
    users.Delete("/:id", handlers.DeleteUser)
}

Main Application

// main.go
package main

import (
    "log"
    
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/yourusername/fiber-api/routes"
)

func main() {
    app := fiber.New(fiber.Config{
        AppName: "User API v1.0.0",
    })
    
    // Middleware
    app.Use(logger.New())
    app.Use(cors.New())
    
    // Routes
    routes.Setup(app)
    
    // Start server
    log.Fatal(app.Listen(":3000"))
}

Request Validation

Proper validation is crucial for API security and data integrity.

Using Go Validator

go get github.com/go-playground/validator/v10
import "github.com/go-playground/validator/v10"

var validate = validator.New()

type CreateUserRequest struct {
    Name     string `json:"name" validate:"required,min=3,max=50"`
    Email    string `json:"email" validate:"required,email"`
    Age      int    `json:"age" validate:"required,min=18,max=120"`
    Password string `json:"password" validate:"required,min=8"`
}

func CreateUser(c *fiber.Ctx) error {
    req := new(CreateUserRequest)
    
    if err := c.BodyParser(req); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Invalid request body",
        })
    }
    
    // Validate
    if err := validate.Struct(req); err != nil {
        errors := err.(validator.ValidationErrors)
        return c.Status(400).JSON(fiber.Map{
            "error": "Validation failed",
            "details": errors.Error(),
        })
    }
    
    // Process valid request...
    return c.Status(201).JSON(fiber.Map{
        "message": "User created",
    })
}

Custom Validation Messages

func formatValidationErrors(err error) map[string]string {
    errors := make(map[string]string)
    
    for _, err := range err.(validator.ValidationErrors) {
        field := err.Field()
        
        switch err.Tag() {
        case "required":
            errors[field] = field + " is required"
        case "email":
            errors[field] = "Invalid email format"
        case "min":
            errors[field] = field + " must be at least " + err.Param()
        case "max":
            errors[field] = field + " must be at most " + err.Param()
        default:
            errors[field] = "Invalid " + field
        }
    }
    
    return errors
}

// Usage in handler
if err := validate.Struct(req); err != nil {
    return c.Status(400).JSON(fiber.Map{
        "error": "Validation failed",
        "details": formatValidationErrors(err),
    })
}

Database Integration

Let's integrate a database using GORM.

Setup GORM with PostgreSQL

go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
// database/connection.go
package database

import (
    "log"
    
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

var DB *gorm.DB

func Connect() {
    dsn := "host=localhost user=postgres password=postgres dbname=fiberdb port=5432 sslmode=disable"
    
    var err error
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    
    if err != nil {
        log.Fatal("Failed to connect to database:", err)
    }
    
    log.Println("Database connected successfully")
}

func Migrate(models ...interface{}) {
    err := DB.AutoMigrate(models...)
    if err != nil {
        log.Fatal("Migration failed:", err)
    }
    log.Println("Database migration completed")
}

Define Database Models

// models/user.go
package models

import "gorm.io/gorm"

type User struct {
    gorm.Model
    Name     string `json:"name" gorm:"not null"`
    Email    string `json:"email" gorm:"unique;not null"`
    Age      int    `json:"age"`
    Password string `json:"-" gorm:"not null"`
}

Update Handlers

// handlers/user.go
package handlers

import (
    "github.com/gofiber/fiber/v2"
    "github.com/yourusername/fiber-api/database"
    "github.com/yourusername/fiber-api/models"
)

func GetUsers(c *fiber.Ctx) error {
    var users []models.User
    
    result := database.DB.Find(&users)
    if result.Error != nil {
        return c.Status(500).JSON(fiber.Map{
            "error": "Failed to fetch users",
        })
    }
    
    return c.JSON(fiber.Map{
        "success": true,
        "data":    users,
    })
}

func GetUser(c *fiber.Ctx) error {
    id := c.Params("id")
    var user models.User
    
    result := database.DB.First(&user, id)
    if result.Error != nil {
        return c.Status(404).JSON(fiber.Map{
            "error": "User not found",
        })
    }
    
    return c.JSON(fiber.Map{
        "success": true,
        "data":    user,
    })
}

func CreateUser(c *fiber.Ctx) error {
    user := new(models.User)
    
    if err := c.BodyParser(user); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Invalid request body",
        })
    }
    
    result := database.DB.Create(user)
    if result.Error != nil {
        return c.Status(500).JSON(fiber.Map{
            "error": "Failed to create user",
        })
    }
    
    return c.Status(201).JSON(fiber.Map{
        "success": true,
        "data":    user,
    })
}

func UpdateUser(c *fiber.Ctx) error {
    id := c.Params("id")
    var user models.User
    
    if err := database.DB.First(&user, id).Error; err != nil {
        return c.Status(404).JSON(fiber.Map{
            "error": "User not found",
        })
    }
    
    if err := c.BodyParser(&user); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Invalid request body",
        })
    }
    
    database.DB.Save(&user)
    
    return c.JSON(fiber.Map{
        "success": true,
        "data":    user,
    })
}

func DeleteUser(c *fiber.Ctx) error {
    id := c.Params("id")
    var user models.User
    
    if err := database.DB.First(&user, id).Error; err != nil {
        return c.Status(404).JSON(fiber.Map{
            "error": "User not found",
        })
    }
    
    database.DB.Delete(&user)
    
    return c.JSON(fiber.Map{
        "success": true,
        "message": "User deleted successfully",
    })
}

Update Main

// main.go
package main

import (
    "log"
    
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/yourusername/fiber-api/database"
    "github.com/yourusername/fiber-api/models"
    "github.com/yourusername/fiber-api/routes"
)

func main() {
    // Connect to database
    database.Connect()
    
    // Run migrations
    database.Migrate(&models.User{})
    
    app := fiber.New()
    
    app.Use(logger.New())
    
    routes.Setup(app)
    
    log.Fatal(app.Listen(":3000"))
}

Error Handling

Implement consistent error handling across your API.

Custom Error Handler

func customErrorHandler(c *fiber.Ctx, err error) error {
    code := fiber.StatusInternalServerError
    message := "Internal Server Error"
    
    if e, ok := err.(*fiber.Error); ok {
        code = e.Code
        message = e.Message
    }
    
    return c.Status(code).JSON(fiber.Map{
        "success": false,
        "error":   message,
    })
}

// Use in app configuration
app := fiber.New(fiber.Config{
    ErrorHandler: customErrorHandler,
})

Custom Error Types

// utils/errors.go
package utils

import "github.com/gofiber/fiber/v2"

type AppError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Details string `json:"details,omitempty"`
}

func (e *AppError) Error() string {
    return e.Message
}

func NewBadRequestError(message string) *AppError {
    return &AppError{
        Code:    fiber.StatusBadRequest,
        Message: message,
    }
}

func NewNotFoundError(message string) *AppError {
    return &AppError{
        Code:    fiber.StatusNotFound,
        Message: message,
    }
}

func NewUnauthorizedError(message string) *AppError {
    return &AppError{
        Code:    fiber.StatusUnauthorized,
        Message: message,
    }
}

// Usage in handlers
func GetUser(c *fiber.Ctx) error {
    id := c.Params("id")
    var user models.User
    
    if err := database.DB.First(&user, id).Error; err != nil {
        return utils.NewNotFoundError("User not found")
    }
    
    return c.JSON(user)
}

Authentication and Authorization

Implement JWT-based authentication.

Install JWT Package

go get github.com/golang-jwt/jwt/v5

Create Auth Handlers

// handlers/auth.go
package handlers

import (
    "time"
    
    "github.com/gofiber/fiber/v2"
    "github.com/golang-jwt/jwt/v5"
    "golang.org/x/crypto/bcrypt"
    "github.com/yourusername/fiber-api/database"
    "github.com/yourusername/fiber-api/models"
)

var jwtSecret = []byte("your-secret-key")

type LoginRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required"`
}

type RegisterRequest struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

func Register(c *fiber.Ctx) error {
    req := new(RegisterRequest)
    
    if err := c.BodyParser(req); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Invalid request",
        })
    }
    
    // Hash password
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{
            "error": "Failed to hash password",
        })
    }
    
    user := models.User{
        Name:     req.Name,
        Email:    req.Email,
        Password: string(hashedPassword),
    }
    
    if err := database.DB.Create(&user).Error; err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Email already exists",
        })
    }
    
    return c.Status(201).JSON(fiber.Map{
        "success": true,
        "message": "User registered successfully",
    })
}

func Login(c *fiber.Ctx) error {
    req := new(LoginRequest)
    
    if err := c.BodyParser(req); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Invalid request",
        })
    }
    
    var user models.User
    if err := database.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
        return c.Status(401).JSON(fiber.Map{
            "error": "Invalid credentials",
        })
    }
    
    // Check password
    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
        return c.Status(401).JSON(fiber.Map{
            "error": "Invalid credentials",
        })
    }
    
    // Create JWT token
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "user_id": user.ID,
        "email":   user.Email,
        "exp":     time.Now().Add(time.Hour * 24).Unix(),
    })
    
    tokenString, err := token.SignedString(jwtSecret)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{
            "error": "Failed to generate token",
        })
    }
    
    return c.JSON(fiber.Map{
        "success": true,
        "token":   tokenString,
        "user":    user,
    })
}

JWT Middleware

// middleware/auth.go
package middleware

import (
    "strings"
    
    "github.com/gofiber/fiber/v2"
    "github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte("your-secret-key")

func AuthRequired(c *fiber.Ctx) error {
    authHeader := c.Get("Authorization")
    
    if authHeader == "" {
        return c.Status(401).JSON(fiber.Map{
            "error": "Missing authorization header",
        })
    }
    
    // Extract token
    tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
    
    // Parse token
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return jwtSecret, nil
    })
    
    if err != nil || !token.Valid {
        return c.Status(401).JSON(fiber.Map{
            "error": "Invalid token",
        })
    }
    
    // Extract claims
    claims := token.Claims.(jwt.MapClaims)
    c.Locals("user_id", claims["user_id"])
    c.Locals("email", claims["email"])
    
    return c.Next()
}

Protected Routes

// routes/routes.go
func Setup(app *fiber.App) {
    api := app.Group("/api/v1")
    
    // Public routes
    api.Post("/register", handlers.Register)
    api.Post("/login", handlers.Login)
    
    // Protected routes
    users := api.Group("/users", middleware.AuthRequired)
    users.Get("/", handlers.GetUsers)
    users.Get("/:id", handlers.GetUser)
    users.Put("/:id", handlers.UpdateUser)
    users.Delete("/:id", handlers.DeleteUser)
}

File Upload and Static Files

Handle file uploads and serve static files.

File Upload

app.Post("/upload", func(c *fiber.Ctx) error {
    // Get file from request
    file, err := c.FormFile("file")
    if err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "No file uploaded",
        })
    }
    
    // Save file
    err = c.SaveFile(file, "./uploads/"+file.Filename)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{
            "error": "Failed to save file",
        })
    }
    
    return c.JSON(fiber.Map{
        "success":  true,
        "filename": file.Filename,
        "size":     file.Size,
    })
})

// Multiple files
app.Post("/upload-multiple", func(c *fiber.Ctx) error {
    form, err := c.MultipartForm()
    if err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Failed to parse form",
        })
    }
    
    files := form.File["files"]
    
    for _, file := range files {
        c.SaveFile(file, "./uploads/"+file.Filename)
    }
    
    return c.JSON(fiber.Map{
        "success": true,
        "count":   len(files),
    })
})

Serve Static Files

// Serve single directory
app.Static("/", "./public")

// Serve with prefix
app.Static("/static", "./public")

// Custom configuration
app.Static("/files", "./uploads", fiber.Static{
    Compress:      true,
    ByteRange:     true,
    Browse:        false,
    CacheDuration: 10 * time.Second,
})

Testing Your API

Write comprehensive tests for your API.

Setup Testing

// handlers/user_test.go
package handlers

import (
    "bytes"
    "encoding/json"
    "net/http/httptest"
    "testing"
    
    "github.com/gofiber/fiber/v2"
    "github.com/stretchr/testify/assert"
)

func TestGetUsers(t *testing.T) {
    app := fiber.New()
    app.Get("/users", GetUsers)
    
    req := httptest.NewRequest("GET", "/users", nil)
    resp, _ := app.Test(req)
    
    assert.Equal(t, 200, resp.StatusCode)
}

func TestCreateUser(t *testing.T) {
    app := fiber.New()
    app.Post("/users", CreateUser)
    
    user := map[string]interface{}{
        "name":  "Test User",
        "email": "test@example.com",
        "age":   25,
    }
    
    body, _ := json.Marshal(user)
    req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    
    resp, _ := app.Test(req)
    
    assert.Equal(t, 201, resp.StatusCode)
}

Run Tests

go test ./...
go test -v ./handlers
go test -cover ./...

Deployment Best Practices

Environment Variables

import (
    "os"
    "github.com/joho/godotenv"
)

func init() {
    godotenv.Load()
}

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "3000"
    }
    
    dbURL := os.Getenv("DATABASE_URL")
    jwtSecret := os.Getenv("JWT_SECRET")
    
    // Use environment variables...
}

Docker Deployment

# Dockerfile
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN go build -o main .

FROM alpine:latest
RUN apk --no-cache add ca-certificates

WORKDIR /root/
COPY --from=builder /app/main .

EXPOSE 3000
CMD ["./main"]
# Build and run
docker build -t fiber-api .
docker run -p 3000:3000 fiber-api

Production Configuration

app := fiber.New(fiber.Config{
    Prefork:       true,  // Enable for production
    ServerHeader:  "Fiber",
    StrictRouting: true,
    CaseSensitive: true,
    ReadTimeout:   time.Second * 10,
    WriteTimeout:  time.Second * 10,
    IdleTimeout:   time.Second * 120,
})

Conclusion

You've now learned how to build production-ready REST APIs with Fiber. From basic routing to advanced features like authentication, database integration, and middleware, Fiber provides everything you need to build fast, scalable web services.

Fiber's Express-like syntax makes it approachable for developers from any background, while its performance characteristics ensure your APIs can handle high traffic loads. The extensive middleware ecosystem and active community provide solutions for most common requirements.

Key Takeaways

  • Performance: Fiber is built on Fasthttp, delivering exceptional speed
  • Developer Experience: Express-like API reduces learning curve
  • Middleware: Rich ecosystem for common functionality
  • Flexibility: Supports various architectures and patterns
  • Production Ready: Robust features for real-world applications

Next Steps

  1. Build Real Projects: Apply these concepts to actual applications
  2. Explore Advanced Topics: WebSockets, GraphQL, microservices
  3. Study Performance: Profiling, optimization, benchmarking
  4. Learn Deployment: CI/CD, monitoring, scaling strategies
  5. Join the Community: Contribute to Fiber and related projects

Additional Resources

Remember, building great APIs is about more than just code—consider security, performance, documentation, and user experience. Test thoroughly, monitor in production, and iterate based on real-world usage.

Happy building with Fiber!

Back to All Posts