| name | fiber |
| description | [Applies to: **/*.go] Definitive guidelines for writing high-performance, secure, and maintainable Fiber applications in Go. Focuses on context immutability, robust error handling, and modular design. |
| source | cursor_mdc |
Fiber Best Practices
Fiber is a high-performance, Express-inspired web framework for Go, built on fasthttp. These rules ensure we leverage Fiber's speed and maintainability while avoiding common pitfalls.
1. Core Principle: fiber.Ctx Immutability
NEVER retain references to fiber.Ctx or any data extracted from it (like c.Params, c.Query, c.Body) after the handler returns. fiber.Ctx values are reused across requests, making their contents ephemeral. Always copy data if you need to store it.
❌ BAD: Retaining ephemeral data
package handlers
import (
"github.com/gofiber/fiber/v2"
"log"
)
type UserRequest struct {
UserID string
}
func GetUserBad(c *fiber.Ctx) error {
req := &UserRequest{
UserID: c.Params("id"),
}
go func() {
log.Printf("Processing user ID: %s", req.UserID)
}()
return c.SendString("Request received (bad example)")
}
✅ GOOD: Copying ephemeral data
package handlers
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils"
"log"
)
type UserRequest struct {
UserID string
}
func GetUserGood(c *fiber.Ctx) error {
userID := utils.CopyString(c.Params("id"))
req := &UserRequest{
UserID: userID,
}
go func() {
log.Printf("Processing user ID: %s", req.UserID)
}()
return c.SendString("Request received (good example)")
}
2. Code Organization and Structure
Organize your application into logical layers: main, config, router, middleware, handlers, services, repository, models, utils.
.
├── cmd/api/main.go # Application entry point
├── config/config.go # Application configuration loading
├── internal/
│ ├── handlers/ # HTTP request handlers
│ │ └── user_handler.go
│ ├── middleware/ # Global and route-specific middleware
│ │ └── security.go
│ ├── models/ # Data structures (structs for DB, JSON, etc.)
│ │ └── user.go
│ ├── repository/ # Database interaction logic
│ │ └── user_repo.go
│ ├── router/ # Centralized route definitions
│ │ └── router.go
│ └── services/ # Business logic layer
│ └── user_service.go
└── pkg/
└── utils/ # Reusable utility functions
└── response.go
3. Security Best Practices
Always apply essential security middleware globally.
package middleware
import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/csrf"
"github.com/gofiber/fiber/v2/middleware/helmet"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/utils"
)
func FiberMiddleware(app *fiber.App) {
app.Use(
recover.New(),
helmet.New(),
cors.New(),
csrf.New(csrf.Config{
KeyLookup: "header:X-Csrf-Token",
CookieName: "__Host-csrf_",
CookieSameSite: "Strict",
Expiration: 3 * time.Hour,
KeyGenerator: utils.UUID,
}),
limiter.New(limiter.Config{
Max: 20,
Expiration: 30 * time.Second,
}),
compress.New(),
logger.New(),
)
}
4. Error Handling
Implement a centralized error handler and use custom error types for clarity.
❌ BAD: Inconsistent error responses
func GetUser(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ID is required"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Database error"})
}
✅ GOOD: Centralized error handling with custom errors
package utils
import "github.com/gofiber/fiber/v2"
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Err error `json:"-"`
}
func (e *AppError) Error() string {
if e.Err != nil {
return e.Message + ": " + e.Err.Error()
}
return e.Message
}
func NewAppError(code int, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
func GlobalErrorHandler(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
message := "Internal Server Error"
if e, ok := err.(*AppError); ok {
code = e.Code
message = e.Message
if e.Err != nil {
c.App().Logger().Errorf("AppError: %s, Original: %v", e.Message, e.Err)
}
} else if e, ok := err.(*fiber.Error); ok {
code = e.Code
message = e.Message
} {
c.App().Logger().Errorf(, err)
}
c.Status(code).JSON(fiber.Map{
: code,
: message,
})
}
app := fiber.New(fiber.Config{
ErrorHandler: utils.GlobalErrorHandler,
})
func GetUser(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.NewAppError(fiber.StatusBadRequest, "User ID is required", nil)
}
user, err := userService.GetUserByID(id)
if err != nil {
return utils.NewAppError(fiber.StatusInternalServerError, "Failed to retrieve user", err)
}
return c.JSON(user)
}
5. API Design: RESTful & Consistent Responses
Design your APIs following REST principles and ensure consistent JSON response structures.
❌ BAD: Inconsistent response formats
✅ GOOD: Standardized JSON responses
package utils
type SuccessResponse struct {
Status int `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func SendSuccess(c *fiber.Ctx, status int, message string, data interface{}) error {
return c.Status(status).JSON(SuccessResponse{
Status: status,
Message: message,
Data: data,
})
}
func GetUser(c *fiber.Ctx) error {
id := c.Params("id")
user, err := userService.GetUserByID(id)
if err != nil {
return utils.NewAppError(fiber.StatusNotFound, "User not found", err)
}
return utils.SendSuccess(c, fiber.StatusOK, "User retrieved successfully", user)
}
func CreateUser(c *fiber.Ctx) error {
user := new(models.User)
if err := c.BodyParser(user); err != nil {
return utils.NewAppError(fiber.StatusBadRequest, "Invalid request body", err)
}
createdUser, err := userService.CreateUser(user)
if err != nil {
return utils.NewAppError(fiber.StatusInternalServerError, "Failed to create user", err)
}
return utils.SendSuccess(c, fiber.StatusCreated, "User created successfully", createdUser)
}
6. Testing Approaches
Use httptest and Fiber's test utilities for robust, table-driven unit tests. Inject dependencies to enable easy mocking.
package handlers_test
import (
"bytes"
"encoding/json"
"errors"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"your_module/internal/handlers"
"your_module/internal/models"
"your_module/internal/services"
"your_module/pkg/utils"
)
type MockUserService struct {
GetUserByIDFunc func(id string) (*models.User, error)
CreateUserFunc func(user *models.User) (*models.User, error)
}
func (m *MockUserService) GetUserByID(id string) (*models.User, error) {
return m.GetUserByIDFunc(id)
}
func (m *MockUserService) CreateUser(user *models.User) (*models.User, error) {
return m.CreateUserFunc(user)
}
func TestGetUser(t *testing.T) {
app := fiber.New(fiber.Config{
ErrorHandler: utils.GlobalErrorHandler,
})
mockService := &MockUserService{}
userHandler := handlers.NewUserHandler(mockService)
app.Get("/users/:id", userHandler.GetUser)
tests := [] {
name
userID
mockReturnUser *models.User
mockReturnErr
expectedStatus
expectedBody
}{
{
name: ,
userID: ,
mockReturnUser: &models.User{ID: , Name: },
mockReturnErr: ,
expectedStatus: fiber.StatusOK,
expectedBody: ,
},
{
name: ,
userID: ,
mockReturnUser: ,
mockReturnErr: errors.New(),
expectedStatus: fiber.StatusNotFound,
expectedBody: ,
},
{
name: ,
userID: ,
mockReturnUser: ,
mockReturnErr: ,
expectedStatus: fiber.StatusBadRequest,
expectedBody: ,
},
}
_, tt := tests {
t.Run(tt.name, {
mockService.GetUserByIDFunc = (*models.User, ) {
assert.Equal(t, tt.userID, id)
tt.mockReturnUser, tt.mockReturnErr
}
req := httptest.NewRequest(fiber.MethodGet, +tt.userID, )
resp, _ := app.Test(req, )
assert.Equal(t, tt.expectedStatus, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
assert.JSONEq(t, tt.expectedBody, (body))
})
}
}