fiber — Cursor rule: guidelines for Go apps on Fiber
A Cursor rule for Fiber projects in Go. It covers context reuse, project layout, security middleware and error handling.
.cursor/rules/fiber.mdcWhat fiber does
The rule ships with globs: **/*.go, so Cursor applies it to every Go file in the workspace. Its central point: a fiber.Ctx and the strings taken from c.Params, c.Query or c.Body live only until the handler returns. Copy them first, utils.CopyString does the job, or a background goroutine will read someone else's request data.
It also sketches a layered layout, from cmd/api/main.go down to internal/handlers, services and repository, with router and middleware folders alongside. On security, the rule wires recover, helmet, cors, csrf, limiter and compress through a single app.Use call. Errors go through a custom AppError type and one GlobalErrorHandler set in fiber.Config.
Examples target Fiber v2, importing from github.com/gofiber/fiber/v2. On a different major version those paths will not resolve, so check the branch your project uses before adopting the snippets.
What it helps with
- keep request parameters alive after the handler returns
- split a Fiber app into handlers, services and repository layers
- attach helmet, cors and csrf before the first route
- return errors in one consistent JSON shape
- throttle incoming requests with the limiter middleware
Example prompts for the agent
- Apply the fiber rule and copy
c.Paramsbefore storing it in a struct - Refactor my
main.gointo the layers the fiber rule describes - Set up
GlobalErrorHandlerandAppErrorthe way the fiber rule says
How to install fiber
Manually: put the files in place
.cursor/rules/fiber.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/fiber.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/fiber.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/fiber.mdManually: put the files in place
.clinerules/fiber.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about fiber
How do I install fiber in Cursor?
Add a file at .cursor/rules/fiber.mdc and put the rule text inside. Cursor loads MDC files from that folder on its own.
What does the fiber rule do?
It gives the agent conventions for Fiber code: copying data out of the context, a layered project structure, global security middleware, and one place to handle errors.
Which Fiber version does it target?
Fiber v2. The samples import github.com/gofiber/fiber/v2 and pull middleware from the middleware/* subpackages.
Source text
fiber.mdc · CC0-1.0
---
description: Definitive guidelines for writing high-performance, secure, and maintainable Fiber applications in Go. Focuses on context immutability, robust error handling, and modular design.
globs: **/*.go
---
# 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
```go
package handlers
import (
"github.com/gofiber/fiber/v2"
"log"
)
// This struct will hold a reference to the ephemeral string
type UserRequest struct {
UserID string
}
func GetUserBad(c *fiber.Ctx) error {
// DANGER: c.Params("id") returns an ephemeral string.
// Storing it directly will lead to data corruption in future requests.
req := &UserRequest{
UserID: c.Params("id"),
}
// Simulating async processing or storage
go func() {
// By the time this goroutine runs, c.Params("id") might have changed
log.Printf("Processing user ID: %s", req.UserID) // Will likely be wrong
}()
return c.SendString("Request received (bad example)")
}
```
✅ GOOD: Copying ephemeral data
```go
package handlers
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils" // Use Fiber's utility for efficient copying
"log"
)
// This struct holds a safe copy of the string
type UserRequest struct {
UserID string
}
func GetUserGood(c *fiber.Ctx) error {
// ALWAYS copy data from c.Params, c.Query, c.Body, etc.
userID := utils.CopyString(c.Params("id"))
req := &UserRequest{
UserID: userID,
}
// Simulating async processing or storage
go func() {
// This is safe; userID is a distinct copy
log.Printf("Processing user ID: %s", req.UserID) // Will always be correct
}()
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.
```go
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"
)
// FiberMiddleware provides essential security and utility middleware.
func FiberMiddleware(app *fiber.App) {
app.Use(
// Recover from panics to prevent server crashes
recover.New(),
// Add security headers
helmet.New(),
// Enable CORS for frontend integration
cors.New(),
// Protect against CSRF attacks
csrf.New(csrf.Config{
KeyLookup: "header:X-Csrf-Token",
CookieName: "__Host-csrf_",
CookieSameSite: "Strict",
Expiration: 3 * time.Hour,
KeyGenerator: utils.UUID,
}),
// Rate limit requests to prevent abuse
limiter.New(limiter.Config{
Max: 20,
Expiration: 30 * time.Second,
// KeyGenerator: func(c *fiber.Ctx) string { return c.IP() }, // Customize if needed
}),
// Compress responses
compress.New(),
// Request logging
logger.New(),
)
}
```
## 4. Error Handling
Implement a centralized error handler and use custom error types for clarity.
❌ BAD: Inconsistent error responses
```go
// handler.go
func GetUser(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ID is required"})
}
// ... database error
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Database error"})
}
```
✅ GOOD: Centralized error handling with custom errors
```go
package utils
import "github.com/gofiber/fiber/v2"
// AppError represents a custom application error
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Err error `json:"-"` // Original error for logging, not exposed to client
}
func (e *AppError) Error() string {
if e.Err != nil {
return e.Message + ": " + e.Err.Error()
}
return e.Message
}
// NewAppError creates a new AppError
func NewAppError(code int, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
// GlobalErrorHandler handles all errors in the application
func GlobalErrorHandler(c *fiber.Ctx, err error) error {
// Default error response
code := fiber.StatusInternalServerError
message := "Internal Server Error"
if e, ok := err.(*AppError); ok {
code = e.Code
message = e.Message
// Log the original error for debugging
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
} else {
// Log unexpected errors
c.App().Logger().Errorf("Unhandled error: %v", err)
}
return c.Status(code).JSON(fiber.Map{
"code": code,
"message": message,
})
}
```
```go
// In your main.go or router setup:
app := fiber.New(fiber.Config{
ErrorHandler: utils.GlobalErrorHandler,
})
// In your handler:
func GetUser(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.NewAppError(fiber.StatusBadRequest, "User ID is required", nil)
}
// Simulate a service call that might return an error
user, err := userService.GetUserByID(id)
if err != nil {
// Wrap service errors into AppError for consistent client response
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
```go
// GET /users/:id
// Success: {"id": "123", "name": "John Doe"}
// Error: "User not found" (plain text)
// POST /users
// Success: "User created" (plain text)
// Error: {"message": "Invalid input"} (JSON)
```
✅ GOOD: Standardized JSON responses
```go
package utils
// SuccessResponse for successful API calls
type SuccessResponse struct {
Status int `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// SendSuccess sends a standardized success response
func SendSuccess(c *fiber.Ctx, status int, message string, data interface{}) error {
return c.Status(status).JSON(SuccessResponse{
Status: status,
Message: message,
Data: data,
})
}
```
```go
// In your handler:
func GetUser(c *fiber.Ctx) error {
id := c.Params("id")
user, err := userService.GetUserByID(id)
if err != nil {
// Error handling via GlobalErrorHandler
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)
}
// ... validation and service call
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.
```go
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" // Mock this
"your_module/pkg/utils"
)
// MockUserService implements the UserService interface for testing
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, // Use global error handler for consistency
})
// Setup mock service
mockService := &MockUserService{}
userHandler := handlers.NewUserHandler(mockService) // Inject mock service
app.Get("/users/:id", userHandler.GetUser)
tests := []struct {
name string
userID string
mockReturnUser *models.User
mockReturnErr error
expectedStatus int
expectedBody string
}{
{
name: "Success",
userID: "1",
mockReturnUser: &models.User{ID: "1", Name: "Test User"},
mockReturnErr: nil,
expectedStatus: fiber.StatusOK,
expectedBody: `{"status":200,"message":"User retrieved successfully","data":{"id":"1","name":"Test User"}}`,
},
{
name: "Not Found",
userID: "2",
mockReturnUser: nil,
mockReturnErr: errors.New("user not found"),
expectedStatus: fiber.StatusNotFound,
expectedBody: `{"code":404,"message":"User not found"}`,
},
{
name: "Invalid ID",
userID: "", // Handled by handler logic
mockReturnUser: nil,
mockReturnErr: nil, // This won't be called if ID is empty
expectedStatus: fiber.StatusBadRequest,
expectedBody: `{"code":400,"message":"User ID is required"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockService.GetUserByIDFunc = func(id string) (*models.User, error) {
assert.Equal(t, tt.userID, id) // Ensure correct ID is passed
return tt.mockReturnUser, tt.mockReturnErr
}
req := httptest.NewRequest(fiber.MethodGet, "/users/"+tt.userID, nil)
resp, _ := app.Test(req, -1) // -1 disables request latency
assert.Equal(t, tt.expectedStatus, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
assert.JSONEq(t, tt.expectedBody, string(body))
})
}
}
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc