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:
- 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"}'
- 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.
- Get All Users (requires authentication):
curl -X GET http://localhost:3000/api/users \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
- Get a Specific User (requires authentication):
curl -X GET http://localhost:3000/api/users/1 \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
- 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"}'
- 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:
- Validation: Add request validation using libraries like go-validator
- Database: Replace the in-memory store with a real database like PostgreSQL
- Logging: Implement structured logging for better debugging
- Rate Limiting: Add rate limiting to prevent abuse
- 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.