Building RESTful APIs with Fiber: A Complete Guide

Building RESTful APIs with Fiber: A Complete Guide

RESTful APIs are the backbone of modern web applications, enabling seamless communication between frontend and backend systems. Among the many Go frameworks available, Fiber has emerged as a popular choice due to its Express-inspired design and high performance. In this guide, I'll walk you through creating a complete RESTful API with Fiber, including CRUD operations and authentication middleware.

What is Fiber?

Fiber is a Go web framework built on top of Fasthttp, designed to minimize memory allocations and maximize performance. Its API is inspired by Express.js, making it familiar to developers coming from Node.js. Fiber offers:

  • Lightning-fast performance
  • Low memory footprint
  • Express-like routing
  • Middleware support
  • Built-in utilities for JSON handling, static file serving, and more

Getting Started

First, let's set up our project and install the necessary dependencies:

// Initialize a Go module
go mod init fiber-rest-api

// Install Fiber
go get github.com/gofiber/fiber/v2

// Install JWT module for authentication
go get github.com/gofiber/jwt/v3
go get github.com/golang-jwt/jwt/v4

Project Structure

Let's organize our API with a clean structure:

fiber-rest-api/
├── main.go
├── handlers/
│   └── user.go
├── middleware/
│   └── auth.go
├── models/
│   └── user.go
└── database/
    └── database.go

Setting Up the Database Connection

Let's start by creating our database connection. For simplicity, we'll use an in-memory map to store our data:

// database/database.go
package database

import (
    "sync"
    "fiber-rest-api/models"
)

// UserStore is our in-memory database
type UserStore struct {
    users map[string]models.User
    mutex sync.RWMutex
    nextID int
}

// NewUserStore creates a new user store
func NewUserStore() *UserStore {
    return &UserStore{
        users: make(map[string]models.User),
        nextID: 1,
    }
}

// Add adds a user to the store
func (s *UserStore) Add(user models.User) models.User {
    s.mutex.Lock()
    defer s.mutex.Unlock()

    // Generate ID
    id := s.nextID
    s.nextID++

    // Set ID and store user
    user.ID = id
    s.users[string(id)] = user

    return user
}

// Get retrieves a user by ID
func (s *UserStore) Get(id string) (models.User, bool) {
    s.mutex.RLock()
    defer s.mutex.RUnlock()

    user, exists := s.users[id]
    return user, exists
}

// GetAll returns all users
func (s *UserStore) GetAll() []models.User {
    s.mutex.RLock()
    defer s.mutex.RUnlock()

    users := make([]models.User, 0, len(s.users))
    for _, user := range s.users {
        users = append(users, user)
    }

    return users
}

// Update updates a user
func (s *UserStore) Update(id string, user models.User) (models.User, bool) {
    s.mutex.Lock()
    defer s.mutex.Unlock()

    if _, exists := s.users[id]; !exists {
        return models.User{}, false
    }

    user.ID = s.users[id].ID  // Preserve ID
    s.users[id] = user

    return user, true
}

// Delete removes a user
func (s *UserStore) Delete(id string) bool {
    s.mutex.Lock()
    defer s.mutex.Unlock()

    if _, exists := s.users[id]; !exists {
        return false
    }

    delete(s.users, id)
    return true
}

Creating Our Models

Next, let's define our user model:

// models/user.go
package models

type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
    Password string `json:"-"` // Don't expose password in JSON responses
}

// CreateUserRequest is used for user creation
type CreateUserRequest struct {
    Username string `json:"username" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=6"`
}

// UpdateUserRequest is used for user updates
type UpdateUserRequest struct {
    Username string `json:"username"`
    Email    string `json:"email" validate:"omitempty,email"`
}

// LoginRequest is used for authentication
type LoginRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required"`
}

// TokenResponse contains the JWT token
type TokenResponse struct {
    Token string `json:"token"`
}

Authentication Middleware

Now, let's implement our authentication middleware using JWT:

// middleware/auth.go
package middleware

import (
    "github.com/gofiber/fiber/v2"
    jwtware "github.com/gofiber/jwt/v3"
    "github.com/golang-jwt/jwt/v4"
    "time"
    "errors"
)

const SecretKey = "your_secret_key_here" // In production, use environment variables

// GenerateToken creates a new JWT token
func GenerateToken(userID int) (string, error) {
    // Create token
    token := jwt.New(jwt.SigningMethodHS256)

    // Set claims
    claims := token.Claims.(jwt.MapClaims)
    claims["user_id"] = userID
    claims["exp"] = time.Now().Add(time.Hour * 72).Unix() // Token expires in 72 hours

    // Generate encoded token
    tokenString, err := token.SignedString([]byte(SecretKey))
    if err != nil {
        return "", err
    }

    return tokenString, nil
}

// Protected creates a JWT middleware
func Protected() fiber.Handler {
    return jwtware.New(jwtware.Config{
        SigningKey:    []byte(SecretKey),
        ErrorHandler: jwtError,
    })
}

func jwtError(c *fiber.Ctx, err error) error {
    return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
        "error": "Unauthorized",
        "message": "Invalid or expired JWT",
    })
}

// ExtractUserID gets the user ID from the token
func ExtractUserID(c *fiber.Ctx) (int, error) {
    user := c.Locals("user").(*jwt.Token)
    claims := user.Claims.(jwt.MapClaims)

    userID, ok := claims["user_id"].(float64)
    if !ok {
        return 0, errors.New("invalid user_id in token")
    }

    return int(userID), nil
}

User Handlers

Now, let's implement our CRUD handlers:

// handlers/user.go
package handlers

import (
    "github.com/gofiber/fiber/v2"
    "fiber-rest-api/database"
    "fiber-rest-api/models"
    "fiber-rest-api/middleware"
    "strconv"
)

type UserHandler struct {
    store *database.UserStore
}

func NewUserHandler(store *database.UserStore) *UserHandler {
    return &UserHandler{
        store: store,
    }
}

// Login authenticates a user and returns a JWT token
func (h *UserHandler) Login(c *fiber.Ctx) error {
    var req models.LoginRequest

    // Parse request body
    if err := c.BodyParser(&req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid request",
        })
    }

    // Normally you would check credentials against a database
    // Here we're just doing a simple check
    users := h.store.GetAll()
    for _, user := range users {
        if user.Email == req.Email && user.Password == req.Password {
            // Generate token
            token, err := middleware.GenerateToken(user.ID)
            if err != nil {
                return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
                    "error": "Could not generate token",
                })
            }

            return c.JSON(models.TokenResponse{
                Token: token,
            })
        }
    }

    return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
        "error": "Invalid credentials",
    })
}

// CreateUser creates a new user
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    var req models.CreateUserRequest

    // Parse request body
    if err := c.BodyParser(&req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid request",
        })
    }

    // Create user (normally you would hash the password!)
    user := models.User{
        Username: req.Username,
        Email:    req.Email,
        Password: req.Password,
    }

    // Save to database
    savedUser := h.store.Add(user)

    return c.Status(fiber.StatusCreated).JSON(savedUser)
}

// GetAllUsers returns all users
func (h *UserHandler) GetAllUsers(c *fiber.Ctx) error {
    users := h.store.GetAll()
    return c.JSON(users)
}

// GetUser returns a specific user
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
    id := c.Params("id")

    user, exists := h.store.Get(id)
    if !exists {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "User not found",
        })
    }

    return c.JSON(user)
}

// UpdateUser updates a user
func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
    id := c.Params("id")

    var req models.UpdateUserRequest
    if err := c.BodyParser(&req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid request",
        })
    }

    // Check if user exists
    existingUser, exists := h.store.Get(id)
    if !exists {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "User not found",
        })
    }

    // Update fields if provided
    if req.Username != "" {
        existingUser.Username = req.Username
    }
    if req.Email != "" {
        existingUser.Email = req.Email
    }

    // Save updated user
    updatedUser, ok := h.store.Update(id, existingUser)
    if !ok {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": "Failed to update user",
        })
    }

    return c.JSON(updatedUser)
}

// DeleteUser deletes a user
func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
    id := c.Params("id")

    // Check if the authenticated user is deleting their own account
    tokenUserID, err := middleware.ExtractUserID(c)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": "Failed to extract user ID",
        })
    }

    requestedID, err := strconv.Atoi(id)
    if err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid user ID",
        })
    }

    if tokenUserID != requestedID {
        return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
            "error": "You can only delete your own account",
        })
    }

    success := h.store.Delete(id)
    if !success {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "User not found",
        })
    }

    return c.SendStatus(fiber.StatusNoContent)
}

Main Application

Finally, let's tie everything together in our main application:

// main.go
package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/gofiber/fiber/v2/middleware/recover"
    "fiber-rest-api/database"
    "fiber-rest-api/handlers"
    "fiber-rest-api/middleware"
    "log"
)

func main() {
    // Create Fiber app
    app := fiber.New(fiber.Config{
        ErrorHandler: func(c *fiber.Ctx, err error) error {
            return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
                "error": "Internal Server Error",
            })
        },
    })

    // Middleware
    app.Use(logger.New())
    app.Use(recover.New())

    // Setup database
    userStore := database.NewUserStore()

    // Setup handlers
    userHandler := handlers.NewUserHandler(userStore)

    // Public routes
    app.Post("/api/login", userHandler.Login)
    app.Post("/api/users", userHandler.CreateUser)

    // Protected routes group
    api := app.Group("/api", middleware.Protected())

    // User routes
    api.Get("/users", userHandler.GetAllUsers)
    api.Get("/users/:id", userHandler.GetUser)
    api.Put("/users/:id", userHandler.UpdateUser)
    api.Delete("/users/:id", userHandler.DeleteUser)

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

Testing the API

Now that our API is complete, we can test it with a tool like cURL or Postman:

  1. Create a User:
   curl -X POST http://localhost:3000/api/users \
     -H "Content-Type: application/json" \
     -d '{"username":"john_doe","email":"john@example.com","password":"secret123"}'
  1. Login:
   curl -X POST http://localhost:3000/api/login \
     -H "Content-Type: application/json" \
     -d '{"email":"john@example.com","password":"secret123"}'

This will return a JWT token that we'll use for authenticated requests.

  1. Get All Users (requires authentication):
   curl -X GET http://localhost:3000/api/users \
     -H "Authorization: Bearer YOUR_JWT_TOKEN"
  1. Get a Specific User (requires authentication):
   curl -X GET http://localhost:3000/api/users/1 \
     -H "Authorization: Bearer YOUR_JWT_TOKEN"
  1. Update a User (requires authentication):
   curl -X PUT http://localhost:3000/api/users/1 \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer YOUR_JWT_TOKEN" \
     -d '{"username":"john_updated"}'
  1. Delete a User (requires authentication):
   curl -X DELETE http://localhost:3000/api/users/1 \
     -H "Authorization: Bearer YOUR_JWT_TOKEN"

Enhancing Your API

While this guide covers the basics, here are some ways to enhance your API:

  1. Validation: Add request validation using libraries like go-validator
  2. Database: Replace the in-memory store with a real database like PostgreSQL
  3. Logging: Implement structured logging for better debugging
  4. Rate Limiting: Add rate limiting to prevent abuse
  5. Swagger Documentation: Generate API documentation with Swagger

Conclusion

Fiber makes building RESTful APIs in Go straightforward and efficient. With its Express-like syntax and powerful features, you can quickly create robust APIs with authentication, CRUD operations, and more. This guide covered the fundamentals, but Fiber offers much more, including WebSocket support, template rendering, and various middleware options.

By following the patterns in this guide, you can build scalable and maintainable APIs that serve as a solid foundation for your web applications.

fiber documentation

Read more