Fiber Framework Guide¶
Applies to: Fiber v2.50+, Go 1.21+, High-Performance REST APIs, Microservices Load when: Working with Fiber projects, Express-like Go APIs, high-throughput services
Overview¶
Fiber is an Express-inspired web framework built on top of Fasthttp, the fastest HTTP engine for Go. It's designed for ease of use with zero memory allocation and performance in mind.
Key Features: - Express-like API (familiar to Node.js developers) - Built on Fasthttp (10x faster than net/http) - Zero memory allocation in hot paths - Built-in middleware collection - WebSocket support - Rate limiting - Template engines
When to use Fiber: - High-throughput APIs requiring maximum performance - Teams familiar with Express.js - Real-time applications with WebSockets - Microservices requiring low latency
Project Structure¶
myapi/
├── cmd/
│ └── api/
│ └── main.go # Application entry point
├── internal/
│ ├── config/
│ │ └── config.go # Configuration management
│ ├── handler/
│ │ ├── handler.go # Handler container
│ │ ├── user_handler.go # User handlers
│ │ └── health_handler.go # Health check handlers
│ ├── middleware/
│ │ ├── auth.go # JWT authentication
│ │ ├── logger.go # Request logging
│ │ └── recover.go # Panic recovery
│ ├── model/
│ │ ├── user.go # User model
│ │ └── dto.go # Data transfer objects
│ ├── repository/
│ │ ├── repository.go # Repository interface
│ │ └── user_repository.go # User repository
│ ├── service/
│ │ ├── service.go # Service container
│ │ ├── user_service.go # User business logic
│ │ └── auth_service.go # Authentication service
│ └── router/
│ └── router.go # Route definitions
├── pkg/
│ ├── validator/
│ │ └── validator.go # Custom validator
│ └── response/
│ └── response.go # Response helpers
├── go.mod
├── go.sum
└── README.md
Core Patterns¶
Application Setup¶
// cmd/api/main.go
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/helmet"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/requestid"
"myapi/internal/config"
"myapi/internal/handler"
"myapi/internal/middleware"
"myapi/internal/repository"
"myapi/internal/router"
"myapi/internal/service"
)
func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Initialize database
db, err := config.NewDatabase(cfg)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Initialize Fiber app with custom config
app := fiber.New(fiber.Config{
AppName: cfg.AppName,
ServerHeader: cfg.AppName,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
IdleTimeout: cfg.IdleTimeout,
BodyLimit: cfg.BodyLimit,
Prefork: cfg.Prefork, // Enable prefork for multi-core
ErrorHandler: customErrorHandler,
})
// Global middleware
app.Use(requestid.New())
app.Use(middleware.Logger())
app.Use(middleware.Recover())
app.Use(cors.New(cors.Config{
AllowOrigins: cfg.CORSAllowOrigins,
AllowMethods: "GET,POST,PUT,DELETE,PATCH,OPTIONS",
AllowHeaders: "Origin,Content-Type,Accept,Authorization",
AllowCredentials: true,
}))
app.Use(helmet.New())
app.Use(limiter.New(limiter.Config{
Max: cfg.RateLimitMax,
Expiration: cfg.RateLimitExpiration,
KeyGenerator: func(c *fiber.Ctx) string {
return c.IP()
},
LimitReached: func(c *fiber.Ctx) error {
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": "Rate limit exceeded",
})
},
}))
// Initialize layers
repos := repository.NewRepositories(db)
services := service.NewServices(repos, cfg)
handlers := handler.NewHandlers(services)
// Setup routes
router.Setup(app, handlers, services.Auth)
// Graceful shutdown
go func() {
if err := app.Listen(":" + cfg.Port); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := app.ShutdownWithContext(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
// Close database
sqlDB, _ := db.DB()
sqlDB.Close()
log.Println("Server exited")
}
// 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{
"error": message,
"code": code,
"request": c.Locals("requestid"),
})
}
Configuration¶
// internal/config/config.go
package config
import (
"os"
"strconv"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type Config struct {
// App
AppName string
Environment string
Port string
Debug bool
// Server
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
BodyLimit int
Prefork bool
// Database
DatabaseURL string
// JWT
JWTSecret string
JWTExpiration time.Duration
JWTRefreshDuration time.Duration
// CORS
CORSAllowOrigins string
// Rate limiting
RateLimitMax int
RateLimitExpiration time.Duration
}
func Load() (*Config, error) {
return &Config{
// App
AppName: getEnv("APP_NAME", "MyAPI"),
Environment: getEnv("ENVIRONMENT", "development"),
Port: getEnv("PORT", "3000"),
Debug: getEnvBool("DEBUG", true),
// Server
ReadTimeout: getEnvDuration("READ_TIMEOUT", 10*time.Second),
WriteTimeout: getEnvDuration("WRITE_TIMEOUT", 10*time.Second),
IdleTimeout: getEnvDuration("IDLE_TIMEOUT", 120*time.Second),
BodyLimit: getEnvInt("BODY_LIMIT", 4*1024*1024), // 4MB
Prefork: getEnvBool("PREFORK", false),
// Database
DatabaseURL: getEnv("DATABASE_URL", "postgres://localhost:5432/myapi?sslmode=disable"),
// JWT
JWTSecret: getEnv("JWT_SECRET", "your-secret-key"),
JWTExpiration: getEnvDuration("JWT_EXPIRATION", 24*time.Hour),
JWTRefreshDuration: getEnvDuration("JWT_REFRESH_DURATION", 7*24*time.Hour),
// CORS
CORSAllowOrigins: getEnv("CORS_ALLOW_ORIGINS", "*"),
// Rate limiting
RateLimitMax: getEnvInt("RATE_LIMIT_MAX", 100),
RateLimitExpiration: getEnvDuration("RATE_LIMIT_EXPIRATION", time.Minute),
}, nil
}
func NewDatabase(cfg *Config) (*gorm.DB, error) {
logLevel := logger.Silent
if cfg.Debug {
logLevel = logger.Info
}
db, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{
Logger: logger.Default.LogMode(logLevel),
})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(25)
sqlDB.SetConnMaxLifetime(5 * time.Minute)
return db, nil
}
// Helper functions
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if i, err := strconv.Atoi(value); err == nil {
return i
}
}
return defaultValue
}
func getEnvBool(key string, defaultValue bool) bool {
if value := os.Getenv(key); value != "" {
if b, err := strconv.ParseBool(value); err == nil {
return b
}
}
return defaultValue
}
func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if d, err := time.ParseDuration(value); err == nil {
return d
}
}
return defaultValue
}
Models and DTOs¶
// internal/model/user.go
package model
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primarykey" json:"id"`
Email string `gorm:"uniqueIndex;not null;size:255" json:"email"`
Password string `gorm:"not null" json:"-"`
Name string `gorm:"not null;size:100" json:"name"`
Role string `gorm:"not null;default:user" json:"role"`
Active bool `gorm:"not null;default:true" json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (User) TableName() string {
return "users"
}
// internal/model/dto.go
package model
// Request DTOs
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email,max=255"`
Password string `json:"password" validate:"required,min=8,max=72"`
Name string `json:"name" validate:"required,min=2,max=100"`
}
type UpdateUserRequest struct {
Email string `json:"email" validate:"omitempty,email,max=255"`
Name string `json:"name" validate:"omitempty,min=2,max=100"`
}
type LoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
// Response DTOs
type UserResponse struct {
ID uint `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Role string `json:"role"`
Active bool `json:"active"`
CreatedAt string `json:"created_at"`
}
type AuthResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int64 `json:"expires_in"`
User UserResponse `json:"user"`
}
type PaginatedResponse struct {
Data interface{} `json:"data"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalItems int64 `json:"total_items"`
TotalPages int `json:"total_pages"`
}
// ToResponse converts User to UserResponse
func (u *User) ToResponse() UserResponse {
return UserResponse{
ID: u.ID,
Email: u.Email,
Name: u.Name,
Role: u.Role,
Active: u.Active,
CreatedAt: u.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
}
Validator¶
// pkg/validator/validator.go
package validator
import (
"reflect"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type CustomValidator struct {
validator *validator.Validate
}
func New() *CustomValidator {
v := validator.New()
// Use JSON tag names in errors
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
return &CustomValidator{validator: v}
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
// ValidationError represents validation error details
type ValidationError struct {
Field string `json:"field"`
Tag string `json:"tag"`
Message string `json:"message"`
}
// ParseValidationErrors extracts validation errors
func ParseValidationErrors(err error) []ValidationError {
var errors []ValidationError
if validationErrs, ok := err.(validator.ValidationErrors); ok {
for _, e := range validationErrs {
errors = append(errors, ValidationError{
Field: e.Field(),
Tag: e.Tag(),
Message: getErrorMessage(e),
})
}
}
return errors
}
func getErrorMessage(e validator.FieldError) string {
switch e.Tag() {
case "required":
return e.Field() + " is required"
case "email":
return e.Field() + " must be a valid email"
case "min":
return e.Field() + " must be at least " + e.Param() + " characters"
case "max":
return e.Field() + " must be at most " + e.Param() + " characters"
default:
return e.Field() + " is invalid"
}
}
// ValidateBody parses and validates request body
func ValidateBody[T any](c *fiber.Ctx, v *CustomValidator) (*T, error) {
var body T
if err := c.BodyParser(&body); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if err := v.Validate(&body); err != nil {
return nil, err
}
return &body, nil
}
Response Helpers¶
// pkg/response/response.go
package response
import (
"github.com/gofiber/fiber/v2"
"myapi/pkg/validator"
)
// Success returns a success response
func Success(c *fiber.Ctx, data interface{}) error {
return c.JSON(fiber.Map{
"success": true,
"data": data,
})
}
// Created returns a 201 created response
func Created(c *fiber.Ctx, data interface{}) error {
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"success": true,
"data": data,
})
}
// NoContent returns a 204 no content response
func NoContent(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
// Error returns an error response
func Error(c *fiber.Ctx, code int, message string) error {
return c.Status(code).JSON(fiber.Map{
"success": false,
"error": message,
})
}
// ValidationError returns a validation error response
func ValidationError(c *fiber.Ctx, err error) error {
errors := validator.ParseValidationErrors(err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Validation failed",
"details": errors,
})
}
// Paginated returns a paginated response
func Paginated(c *fiber.Ctx, data interface{}, page, pageSize int, total int64) error {
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return c.JSON(fiber.Map{
"success": true,
"data": data,
"meta": fiber.Map{
"page": page,
"page_size": pageSize,
"total_items": total,
"total_pages": totalPages,
},
})
}
Repository¶
// internal/repository/repository.go
package repository
import "gorm.io/gorm"
type Repositories struct {
User UserRepository
}
func NewRepositories(db *gorm.DB) *Repositories {
return &Repositories{
User: NewUserRepository(db),
}
}
// internal/repository/user_repository.go
package repository
import (
"context"
"gorm.io/gorm"
"myapi/internal/model"
)
type UserRepository interface {
Create(ctx context.Context, user *model.User) error
FindByID(ctx context.Context, id uint) (*model.User, error)
FindByEmail(ctx context.Context, email string) (*model.User, error)
FindAll(ctx context.Context, page, pageSize int) ([]model.User, int64, error)
Update(ctx context.Context, user *model.User) error
Delete(ctx context.Context, id uint) error
ExistsByEmail(ctx context.Context, email string) (bool, error)
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Create(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *userRepository) FindByID(ctx context.Context, id uint) (*model.User, error) {
var user model.User
err := r.db.WithContext(ctx).First(&user, id).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*model.User, error) {
var user model.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindAll(ctx context.Context, page, pageSize int) ([]model.User, int64, error) {
var users []model.User
var total int64
r.db.WithContext(ctx).Model(&model.User{}).Count(&total)
offset := (page - 1) * pageSize
err := r.db.WithContext(ctx).
Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&users).Error
return users, total, err
}
func (r *userRepository) Update(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Save(user).Error
}
func (r *userRepository) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.User{}, id).Error
}
func (r *userRepository) ExistsByEmail(ctx context.Context, email string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&model.User{}).Where("email = ?", email).Count(&count).Error
return count > 0, err
}
Service Layer¶
// internal/service/service.go
package service
import (
"myapi/internal/config"
"myapi/internal/repository"
)
type Services struct {
User UserService
Auth AuthService
}
func NewServices(repos *repository.Repositories, cfg *config.Config) *Services {
authService := NewAuthService(cfg)
return &Services{
User: NewUserService(repos.User, authService),
Auth: authService,
}
}
// internal/service/auth_service.go
package service
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"myapi/internal/config"
"myapi/internal/model"
)
var (
ErrInvalidToken = errors.New("invalid token")
ErrExpiredToken = errors.New("token has expired")
ErrInvalidCredentials = errors.New("invalid credentials")
)
type AuthService interface {
GenerateTokens(user *model.User) (*model.AuthResponse, error)
ValidateToken(tokenString string) (*Claims, error)
HashPassword(password string) (string, error)
CheckPassword(hashedPassword, password string) bool
}
type Claims struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
type authService struct {
secret []byte
expiration time.Duration
refreshDuration time.Duration
}
func NewAuthService(cfg *config.Config) AuthService {
return &authService{
secret: []byte(cfg.JWTSecret),
expiration: cfg.JWTExpiration,
refreshDuration: cfg.JWTRefreshDuration,
}
}
func (s *authService) GenerateTokens(user *model.User) (*model.AuthResponse, error) {
now := time.Now()
expiresAt := now.Add(s.expiration)
claims := &Claims{
UserID: user.ID,
Email: user.Email,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(now),
Subject: user.Email,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
accessToken, err := token.SignedString(s.secret)
if err != nil {
return nil, err
}
// Generate refresh token with longer expiration
refreshClaims := &Claims{
UserID: user.ID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(s.refreshDuration)),
IssuedAt: jwt.NewNumericDate(now),
},
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshTokenString, err := refreshToken.SignedString(s.secret)
if err != nil {
return nil, err
}
return &model.AuthResponse{
AccessToken: accessToken,
RefreshToken: refreshTokenString,
ExpiresIn: int64(s.expiration.Seconds()),
User: user.ToResponse(),
}, nil
}
func (s *authService) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, ErrInvalidToken
}
return s.secret, nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, ErrInvalidToken
}
return claims, nil
}
func (s *authService) HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func (s *authService) CheckPassword(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
// internal/service/user_service.go
package service
import (
"context"
"errors"
"gorm.io/gorm"
"myapi/internal/model"
"myapi/internal/repository"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrUserAlreadyExists = errors.New("user already exists")
)
type UserService interface {
Create(ctx context.Context, req *model.CreateUserRequest) (*model.User, error)
GetByID(ctx context.Context, id uint) (*model.User, error)
GetAll(ctx context.Context, page, pageSize int) ([]model.User, int64, error)
Update(ctx context.Context, id uint, req *model.UpdateUserRequest) (*model.User, error)
Delete(ctx context.Context, id uint) error
Authenticate(ctx context.Context, req *model.LoginRequest) (*model.AuthResponse, error)
}
type userService struct {
repo repository.UserRepository
auth AuthService
}
func NewUserService(repo repository.UserRepository, auth AuthService) UserService {
return &userService{
repo: repo,
auth: auth,
}
}
func (s *userService) Create(ctx context.Context, req *model.CreateUserRequest) (*model.User, error) {
exists, err := s.repo.ExistsByEmail(ctx, req.Email)
if err != nil {
return nil, err
}
if exists {
return nil, ErrUserAlreadyExists
}
hashedPassword, err := s.auth.HashPassword(req.Password)
if err != nil {
return nil, err
}
user := &model.User{
Email: req.Email,
Password: hashedPassword,
Name: req.Name,
Role: "user",
Active: true,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *userService) GetByID(ctx context.Context, id uint) (*model.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return user, nil
}
func (s *userService) GetAll(ctx context.Context, page, pageSize int) ([]model.User, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
return s.repo.FindAll(ctx, page, pageSize)
}
func (s *userService) Update(ctx context.Context, id uint, req *model.UpdateUserRequest) (*model.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
if req.Email != "" && req.Email != user.Email {
exists, err := s.repo.ExistsByEmail(ctx, req.Email)
if err != nil {
return nil, err
}
if exists {
return nil, ErrUserAlreadyExists
}
user.Email = req.Email
}
if req.Name != "" {
user.Name = req.Name
}
if err := s.repo.Update(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *userService) Delete(ctx context.Context, id uint) error {
_, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrUserNotFound
}
return err
}
return s.repo.Delete(ctx, id)
}
func (s *userService) Authenticate(ctx context.Context, req *model.LoginRequest) (*model.AuthResponse, error) {
user, err := s.repo.FindByEmail(ctx, req.Email)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrInvalidCredentials
}
return nil, err
}
if !s.auth.CheckPassword(user.Password, req.Password) {
return nil, ErrInvalidCredentials
}
if !user.Active {
return nil, errors.New("account is deactivated")
}
return s.auth.GenerateTokens(user)
}
Handlers¶
// internal/handler/handler.go
package handler
import (
"myapi/internal/service"
"myapi/pkg/validator"
)
type Handlers struct {
User *UserHandler
Health *HealthHandler
validator *validator.CustomValidator
}
func NewHandlers(services *service.Services) *Handlers {
v := validator.New()
return &Handlers{
User: NewUserHandler(services.User, v),
Health: NewHealthHandler(),
validator: v,
}
}
// internal/handler/user_handler.go
package handler
import (
"errors"
"strconv"
"github.com/gofiber/fiber/v2"
"myapi/internal/model"
"myapi/internal/service"
"myapi/pkg/response"
"myapi/pkg/validator"
)
type UserHandler struct {
service service.UserService
validator *validator.CustomValidator
}
func NewUserHandler(service service.UserService, v *validator.CustomValidator) *UserHandler {
return &UserHandler{
service: service,
validator: v,
}
}
// Create godoc
// @Summary Create a new user
// @Tags users
// @Accept json
// @Produce json
// @Param request body model.CreateUserRequest true "User data"
// @Success 201 {object} model.UserResponse
// @Failure 400 {object} map[string]interface{}
// @Failure 409 {object} map[string]interface{}
// @Router /api/v1/users [post]
func (h *UserHandler) Create(c *fiber.Ctx) error {
req, err := validator.ValidateBody[model.CreateUserRequest](c, h.validator)
if err != nil {
if _, ok := err.(*fiber.Error); ok {
return err
}
return response.ValidationError(c, err)
}
user, err := h.service.Create(c.Context(), req)
if err != nil {
if errors.Is(err, service.ErrUserAlreadyExists) {
return response.Error(c, fiber.StatusConflict, "User already exists")
}
return err
}
return response.Created(c, user.ToResponse())
}
// GetByID godoc
// @Summary Get user by ID
// @Tags users
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} model.UserResponse
// @Failure 404 {object} map[string]interface{}
// @Router /api/v1/users/{id} [get]
func (h *UserHandler) GetByID(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return response.Error(c, fiber.StatusBadRequest, "Invalid user ID")
}
user, err := h.service.GetByID(c.Context(), uint(id))
if err != nil {
if errors.Is(err, service.ErrUserNotFound) {
return response.Error(c, fiber.StatusNotFound, "User not found")
}
return err
}
return response.Success(c, user.ToResponse())
}
// GetAll godoc
// @Summary List all users
// @Tags users
// @Produce json
// @Param page query int false "Page number" default(1)
// @Param page_size query int false "Page size" default(20)
// @Success 200 {object} model.PaginatedResponse
// @Router /api/v1/users [get]
func (h *UserHandler) GetAll(c *fiber.Ctx) error {
page := c.QueryInt("page", 1)
pageSize := c.QueryInt("page_size", 20)
users, total, err := h.service.GetAll(c.Context(), page, pageSize)
if err != nil {
return err
}
responses := make([]model.UserResponse, len(users))
for i, user := range users {
responses[i] = user.ToResponse()
}
return response.Paginated(c, responses, page, pageSize, total)
}
// Update godoc
// @Summary Update user
// @Tags users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Param request body model.UpdateUserRequest true "User data"
// @Success 200 {object} model.UserResponse
// @Failure 400 {object} map[string]interface{}
// @Failure 404 {object} map[string]interface{}
// @Router /api/v1/users/{id} [put]
func (h *UserHandler) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return response.Error(c, fiber.StatusBadRequest, "Invalid user ID")
}
req, err := validator.ValidateBody[model.UpdateUserRequest](c, h.validator)
if err != nil {
if _, ok := err.(*fiber.Error); ok {
return err
}
return response.ValidationError(c, err)
}
user, err := h.service.Update(c.Context(), uint(id), req)
if err != nil {
if errors.Is(err, service.ErrUserNotFound) {
return response.Error(c, fiber.StatusNotFound, "User not found")
}
if errors.Is(err, service.ErrUserAlreadyExists) {
return response.Error(c, fiber.StatusConflict, "Email already in use")
}
return err
}
return response.Success(c, user.ToResponse())
}
// Delete godoc
// @Summary Delete user
// @Tags users
// @Param id path int true "User ID"
// @Success 204
// @Failure 404 {object} map[string]interface{}
// @Router /api/v1/users/{id} [delete]
func (h *UserHandler) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return response.Error(c, fiber.StatusBadRequest, "Invalid user ID")
}
if err := h.service.Delete(c.Context(), uint(id)); err != nil {
if errors.Is(err, service.ErrUserNotFound) {
return response.Error(c, fiber.StatusNotFound, "User not found")
}
return err
}
return response.NoContent(c)
}
// Login godoc
// @Summary Authenticate user
// @Tags auth
// @Accept json
// @Produce json
// @Param request body model.LoginRequest true "Credentials"
// @Success 200 {object} model.AuthResponse
// @Failure 401 {object} map[string]interface{}
// @Router /api/v1/auth/login [post]
func (h *UserHandler) Login(c *fiber.Ctx) error {
req, err := validator.ValidateBody[model.LoginRequest](c, h.validator)
if err != nil {
if _, ok := err.(*fiber.Error); ok {
return err
}
return response.ValidationError(c, err)
}
authResponse, err := h.service.Authenticate(c.Context(), req)
if err != nil {
if errors.Is(err, service.ErrInvalidCredentials) {
return response.Error(c, fiber.StatusUnauthorized, "Invalid credentials")
}
return err
}
return response.Success(c, authResponse)
}
// GetProfile godoc
// @Summary Get current user profile
// @Tags auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} model.UserResponse
// @Router /api/v1/auth/profile [get]
func (h *UserHandler) GetProfile(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
user, err := h.service.GetByID(c.Context(), userID)
if err != nil {
return err
}
return response.Success(c, user.ToResponse())
}
// internal/handler/health_handler.go
package handler
import (
"github.com/gofiber/fiber/v2"
)
type HealthHandler struct{}
func NewHealthHandler() *HealthHandler {
return &HealthHandler{}
}
func (h *HealthHandler) Health(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "healthy",
"service": "myapi",
})
}
func (h *HealthHandler) Ready(c *fiber.Ctx) error {
// Check database connection, external services, etc.
return c.JSON(fiber.Map{
"status": "ready",
})
}
Middleware¶
// internal/middleware/auth.go
package middleware
import (
"strings"
"github.com/gofiber/fiber/v2"
"myapi/internal/service"
)
func Auth(authService service.AuthService) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
return fiber.NewError(fiber.StatusUnauthorized, "Missing authorization header")
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid authorization format")
}
claims, err := authService.ValidateToken(parts[1])
if err != nil {
if err == service.ErrExpiredToken {
return fiber.NewError(fiber.StatusUnauthorized, "Token has expired")
}
return fiber.NewError(fiber.StatusUnauthorized, "Invalid token")
}
// Store user info in context
c.Locals("userID", claims.UserID)
c.Locals("userEmail", claims.Email)
c.Locals("userRole", claims.Role)
return c.Next()
}
}
// RequireRole middleware checks if user has required role
func RequireRole(roles ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
userRole, ok := c.Locals("userRole").(string)
if !ok {
return fiber.NewError(fiber.StatusForbidden, "Access denied")
}
for _, role := range roles {
if userRole == role {
return c.Next()
}
}
return fiber.NewError(fiber.StatusForbidden, "Insufficient permissions")
}
}
// internal/middleware/logger.go
package middleware
import (
"log"
"time"
"github.com/gofiber/fiber/v2"
)
func Logger() fiber.Handler {
return func(c *fiber.Ctx) error {
start := time.Now()
// Process request
err := c.Next()
// Log request
log.Printf(
"[%s] %s %s %d %s",
c.Method(),
c.Path(),
c.IP(),
c.Response().StatusCode(),
time.Since(start),
)
return err
}
}
// internal/middleware/recover.go
package middleware
import (
"log"
"runtime/debug"
"github.com/gofiber/fiber/v2"
)
func Recover() fiber.Handler {
return func(c *fiber.Ctx) error {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic recovered: %v\n%s", r, debug.Stack())
c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
}()
return c.Next()
}
}
Router¶
// internal/router/router.go
package router
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/swagger"
"myapi/internal/handler"
"myapi/internal/middleware"
"myapi/internal/service"
)
func Setup(app *fiber.App, h *handler.Handlers, authService service.AuthService) {
// Health checks
app.Get("/health", h.Health.Health)
app.Get("/ready", h.Health.Ready)
// Swagger docs
app.Get("/swagger/*", swagger.HandlerDefault)
// API v1
v1 := app.Group("/api/v1")
// Auth routes (public)
auth := v1.Group("/auth")
auth.Post("/login", h.User.Login)
auth.Post("/register", h.User.Create)
// Protected auth routes
authProtected := auth.Group("", middleware.Auth(authService))
authProtected.Get("/profile", h.User.GetProfile)
// User routes (protected)
users := v1.Group("/users", middleware.Auth(authService))
users.Get("/", h.User.GetAll)
users.Get("/:id", h.User.GetByID)
users.Put("/:id", h.User.Update)
users.Delete("/:id", middleware.RequireRole("admin"), h.User.Delete)
}
Testing¶
Handler Tests¶
// internal/handler/user_handler_test.go
package handler
import (
"bytes"
"encoding/json"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"myapi/internal/model"
"myapi/internal/service"
"myapi/pkg/validator"
)
type MockUserService struct {
mock.Mock
}
func (m *MockUserService) Create(ctx context.Context, req *model.CreateUserRequest) (*model.User, error) {
args := m.Called(ctx, req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.User), args.Error(1)
}
func (m *MockUserService) GetByID(ctx context.Context, id uint) (*model.User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.User), args.Error(1)
}
func (m *MockUserService) GetAll(ctx context.Context, page, pageSize int) ([]model.User, int64, error) {
args := m.Called(ctx, page, pageSize)
return args.Get(0).([]model.User), args.Get(1).(int64), args.Error(2)
}
func (m *MockUserService) Update(ctx context.Context, id uint, req *model.UpdateUserRequest) (*model.User, error) {
args := m.Called(ctx, id, req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.User), args.Error(1)
}
func (m *MockUserService) Delete(ctx context.Context, id uint) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockUserService) Authenticate(ctx context.Context, req *model.LoginRequest) (*model.AuthResponse, error) {
args := m.Called(ctx, req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.AuthResponse), args.Error(1)
}
func setupTestApp(mockService *MockUserService) *fiber.App {
app := fiber.New()
v := validator.New()
handler := NewUserHandler(mockService, v)
app.Post("/users", handler.Create)
app.Get("/users/:id", handler.GetByID)
app.Get("/users", handler.GetAll)
return app
}
func TestUserHandler_Create(t *testing.T) {
mockService := new(MockUserService)
app := setupTestApp(mockService)
t.Run("success", func(t *testing.T) {
user := &model.User{
ID: 1,
Email: "test@example.com",
Name: "Test User",
Role: "user",
}
mockService.On("Create", mock.Anything, mock.MatchedBy(func(req *model.CreateUserRequest) bool {
return req.Email == "test@example.com"
})).Return(user, nil).Once()
body, _ := json.Marshal(map[string]string{
"email": "test@example.com",
"password": "password123",
"name": "Test User",
})
req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, fiber.StatusCreated, resp.StatusCode)
mockService.AssertExpectations(t)
})
t.Run("validation error", func(t *testing.T) {
body, _ := json.Marshal(map[string]string{
"email": "invalid",
})
req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode)
})
t.Run("user already exists", func(t *testing.T) {
mockService.On("Create", mock.Anything, mock.Anything).
Return(nil, service.ErrUserAlreadyExists).Once()
body, _ := json.Marshal(map[string]string{
"email": "existing@example.com",
"password": "password123",
"name": "Test User",
})
req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, fiber.StatusConflict, resp.StatusCode)
mockService.AssertExpectations(t)
})
}
func TestUserHandler_GetByID(t *testing.T) {
mockService := new(MockUserService)
app := setupTestApp(mockService)
t.Run("success", func(t *testing.T) {
user := &model.User{
ID: 1,
Email: "test@example.com",
Name: "Test User",
}
mockService.On("GetByID", mock.Anything, uint(1)).Return(user, nil).Once()
req := httptest.NewRequest("GET", "/users/1", nil)
resp, _ := app.Test(req)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
mockService.AssertExpectations(t)
})
t.Run("not found", func(t *testing.T) {
mockService.On("GetByID", mock.Anything, uint(999)).
Return(nil, service.ErrUserNotFound).Once()
req := httptest.NewRequest("GET", "/users/999", nil)
resp, _ := app.Test(req)
assert.Equal(t, fiber.StatusNotFound, resp.StatusCode)
mockService.AssertExpectations(t)
})
}
Commands¶
# Run development server
go run cmd/api/main.go
# Build
go build -o bin/api cmd/api/main.go
# Run tests
go test ./...
# Run tests with coverage
go test -cover ./...
# Run tests with race detector
go test -race ./...
# Generate swagger docs (with swag)
swag init -g cmd/api/main.go
# Database migrations (with golang-migrate)
migrate -path migrations -database "$DATABASE_URL" up
migrate -path migrations -database "$DATABASE_URL" down 1
# Lint
golangci-lint run
# Build for production
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o bin/api cmd/api/main.go
Dependencies¶
# Core
go get github.com/gofiber/fiber/v2
go get github.com/gofiber/swagger
# Database
go get gorm.io/gorm
go get gorm.io/driver/postgres
# Validation
go get github.com/go-playground/validator/v10
# JWT
go get github.com/golang-jwt/jwt/v5
# Password hashing
go get golang.org/x/crypto/bcrypt
# Testing
go get github.com/stretchr/testify
# Documentation
go get github.com/swaggo/swag/cmd/swag
Best Practices¶
Performance¶
- ✓ Enable Prefork for multi-core utilization
- ✓ Use fasthttp's zero-allocation patterns
- ✓ Configure appropriate timeouts
- ✓ Use connection pooling for database
- ✓ Implement caching for frequently accessed data
- ✓ Use
fiber.Ctx.Context()for request context
Security¶
- ✓ Use Helmet middleware for security headers
- ✓ Implement rate limiting
- ✓ Validate all inputs
- ✓ Use CORS middleware appropriately
- ✓ Hash passwords with bcrypt
- ✓ Use HTTPS in production
Error Handling¶
- ✓ Use custom error handler
- ✓ Return consistent error responses
- ✓ Log errors with context
- ✓ Don't expose internal errors to clients
Code Organization¶
- ✓ Follow clean architecture
- ✓ Use dependency injection
- ✓ Keep handlers thin
- ✓ Business logic in services
- ✓ Data access in repositories
Fiber vs Gin vs Echo¶
| Feature | Fiber | Gin | Echo |
|---|---|---|---|
| Performance | Fastest (fasthttp) | Fast (net/http) | Fast (net/http) |
| API Style | Express-like | Gin-specific | Echo-specific |
| Memory | Zero alloc | Low alloc | Low alloc |
| Learning Curve | Easy (Express devs) | Moderate | Moderate |
| Ecosystem | Growing | Mature | Mature |
| WebSockets | Built-in | Plugin | Plugin |