backend-go/fiber-project-starter/SKILL.md
Scaffold and develop high-performance REST APIs using the Fiber v2 web framework with GORM/SQLX, Express-inspired routing, and idiomatic Go patterns.
npx skillsauth add achreftlili/deep-dev-skills fiber-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold and develop high-performance REST APIs using the Fiber v2 web framework with GORM/SQLX, Express-inspired routing, and idiomatic Go patterns.
go version)golangci-lint for linting (go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest)swag CLI for Swagger generation (go install github.com/swaggo/swag/cmd/swag@latest)mkdir -p myapp && cd myapp
go mod init github.com/yourorg/myapp
go get github.com/gofiber/fiber/v2@latest
go get github.com/gofiber/swagger@latest
go get github.com/gofiber/fiber/v2/middleware/cors@latest
go get github.com/gofiber/fiber/v2/middleware/logger@latest
go get github.com/gofiber/fiber/v2/middleware/recover@latest
go get gorm.io/gorm@latest
go get gorm.io/driver/postgres@latest
go get github.com/go-playground/validator/v10@latest
myapp/
├── cmd/
│ └── server/
│ └── main.go # Entrypoint, wires dependencies
├── internal/
│ ├── config/
│ │ └── config.go # Env-based configuration
│ ├── database/
│ │ └── database.go # DB connection setup (GORM or SQLX)
│ ├── handler/
│ │ ├── user.go # User HTTP handlers
│ │ ├── auth.go # Auth handlers
│ │ └── health.go # Health check
│ ├── middleware/
│ │ └── auth.go # JWT auth middleware
│ ├── model/
│ │ └── user.go # Domain/DB models
│ ├── repository/
│ │ └── user.go # Data access layer
│ ├── service/
│ │ └── user.go # Business logic
│ └── router/
│ └── router.go # Route definitions and middleware
├── static/ # Static files (if serving)
├── docs/ # Generated by swag init
├── .env.example # Environment variable template
├── go.mod
├── go.sum
└── Makefile
fasthttp under the hood, not net/http. This means *fiber.Ctx is NOT compatible with context.Context patterns directly -- use c.UserContext() to get the request context.Ctx is pooled and reused. Never store *fiber.Ctx references beyond the handler lifecycle. Copy values you need.c.BodyParser() for JSON/form binding, c.QueryParser() for query params, c.ParamsInt() for URL params.app.Group() and apply middleware per group.c.Status(code).JSON(...) -- Fiber handlers return error but the error is for framework-level failures, not HTTP error responses.fiber.Map as a shorthand for map[string]interface{} in JSON responses.// cmd/server/main.go
package main
import (
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/yourorg/myapp/internal/config"
"github.com/yourorg/myapp/internal/database"
"github.com/yourorg/myapp/internal/router"
)
// @title MyApp API
// @version 1.0
// @description A sample Fiber API server.
// @host localhost:8080
// @BasePath /api/v1
func main() {
cfg := config.Load()
db, err := database.Connect(cfg.DatabaseURL)
if err != nil {
slog.Error("database connection failed", "error", err)
os.Exit(1)
}
app := router.Setup(cfg, db)
// Graceful shutdown
go func() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("shutting down server...")
if err := app.Shutdown(); err != nil {
slog.Error("shutdown error", "error", err)
}
}()
slog.Info("server starting", "port", cfg.Port)
if err := app.Listen(":" + cfg.Port); err != nil {
slog.Error("listen failed", "error", err)
os.Exit(1)
}
slog.Info("server exited")
}
// internal/router/router.go
package router
import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/limiter"
fiberLogger "github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/swagger"
"gorm.io/gorm"
"github.com/yourorg/myapp/internal/config"
"github.com/yourorg/myapp/internal/handler"
"github.com/yourorg/myapp/internal/middleware"
"github.com/yourorg/myapp/internal/repository"
"github.com/yourorg/myapp/internal/service"
)
func Setup(cfg *config.Config, db *gorm.DB) *fiber.App {
app := fiber.New(fiber.Config{
AppName: "MyApp",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
BodyLimit: 4 * 1024 * 1024, // 4MB
// Prefork: true, // Enable for production multi-process mode
ErrorHandler: customErrorHandler,
})
// Global middleware
app.Use(recover.New())
app.Use(fiberLogger.New())
app.Use(cors.New(cors.Config{
AllowOrigins: cfg.CORSOrigin,
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
AllowHeaders: "Origin,Content-Type,Authorization",
}))
// Static file serving
app.Static("/static", "./static", fiber.Static{
Compress: true,
MaxAge: 3600,
})
// Swagger
app.Get("/swagger/*", swagger.HandlerDefault)
// Health check
app.Get("/healthz", handler.Healthz)
// Wire dependencies
userRepo := repository.NewUserRepository(db)
userSvc := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userSvc)
authHandler := handler.NewAuthHandler(userSvc, cfg)
// API v1
v1 := app.Group("/api/v1")
// Rate limiter on auth routes
authGroup := v1.Group("/auth")
authGroup.Use(limiter.New(limiter.Config{
Max: 10,
Expiration: 1 * time.Minute,
}))
authGroup.Post("/login", authHandler.Login)
authGroup.Post("/register", authHandler.Register)
// Protected routes
protected := v1.Group("/", middleware.AuthRequired(cfg.JWTSecret))
protected.Get("/users", userHandler.List)
protected.Get("/users/:id", userHandler.GetByID)
protected.Put("/users/:id", userHandler.Update)
protected.Delete("/users/:id", userHandler.Delete)
return app
}
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,
})
}
// internal/handler/user.go
package handler
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/yourorg/myapp/internal/service"
)
var validate = validator.New()
type UserHandler struct {
svc service.UserService
}
func NewUserHandler(svc service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,gte=1,lte=150"`
}
type UserResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
// GetByID godoc
// @Summary Get user by ID
// @Tags users
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} UserResponse
// @Failure 404 {object} map[string]string
// @Router /users/{id} [get]
func (h *UserHandler) GetByID(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid id parameter")
}
ctx := c.UserContext()
user, err := h.svc.GetByID(ctx, uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, "user not found")
}
return c.JSON(UserResponse{
ID: user.ID,
Name: user.Name,
Email: user.Email,
Age: user.Age,
})
}
// List with query-based pagination
func (h *UserHandler) List(c *fiber.Ctx) error {
type PaginationQuery struct {
Page int `query:"page" validate:"omitempty,gte=1"`
PageSize int `query:"page_size" validate:"omitempty,gte=1,lte=100"`
}
q := PaginationQuery{Page: 1, PageSize: 20}
if err := c.QueryParser(&q); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid query parameters")
}
if err := validate.Struct(&q); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "validation failed")
}
ctx := c.UserContext()
users, total, err := h.svc.List(ctx, q.Page, q.PageSize)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to list users")
}
return c.JSON(fiber.Map{
"data": users,
"total": total,
"page": q.Page,
"page_size": q.PageSize,
})
}
func (h *UserHandler) Update(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid id parameter")
}
var req struct {
Name string `json:"name" validate:"omitempty,min=2,max=100"`
Age int `json:"age" validate:"omitempty,gte=1,lte=150"`
}
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid request body")
}
if err := validate.Struct(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "validation failed")
}
ctx := c.UserContext()
user, err := h.svc.Update(ctx, uint(id), req.Name, req.Age)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to update user")
}
return c.JSON(user)
}
func (h *UserHandler) Delete(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid id parameter")
}
ctx := c.UserContext()
if err := h.svc.Delete(ctx, uint(id)); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to delete user")
}
return c.SendStatus(fiber.StatusNoContent)
}
// Healthz handler (standalone, no struct needed)
func Healthz(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "ok"})
}
// internal/middleware/auth.go
package middleware
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
func AuthRequired(secret string) fiber.Handler {
return func(c *fiber.Ctx) error {
header := c.Get("Authorization")
if header == "" {
return fiber.NewError(fiber.StatusUnauthorized, "missing authorization header")
}
parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization format")
}
token, err := jwt.Parse(parts[1], func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(secret), nil
})
if err != nil || !token.Valid {
return fiber.NewError(fiber.StatusUnauthorized, "invalid token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "invalid claims")
}
// Store user ID in Fiber locals (available for this request only)
c.Locals("userID", claims["sub"])
return c.Next()
}
}
// internal/database/database.go
package database
import (
"log/slog"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/yourorg/myapp/internal/model"
)
func Connect(dsn string) (*gorm.DB, error) {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(5)
// Auto-migrate in development only
if err := db.AutoMigrate(&model.User{}); err != nil {
slog.Error("auto-migrate failed", "error", err)
return nil, err
}
return db, nil
}
// internal/database/database_sqlx.go
package database
import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
func ConnectSQLX(dsn string) (*sqlx.DB, error) {
db, err := sqlx.Connect("postgres", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
return db, nil
}
.env.example to .env and fill in valuesgo mod downloadmigrate -path migrations -database "$DATABASE_URL" upgo run ./cmd/servercurl http://localhost:8080/healthz# Run the server
go run ./cmd/server
# Run with prefork mode (multi-process, for production benchmarking)
PREFORK=true go run ./cmd/server
# Run tests
go test ./... -v -race -count=1
# Run tests with coverage
go test ./... -coverprofile=coverage.out && go tool cover -html=coverage.out
# Generate Swagger docs
swag init -g cmd/server/main.go -o docs
# Lint
golangci-lint run ./...
# Build binary
go build -o bin/server ./cmd/server
# Tidy dependencies
go mod tidy
# Format code
gofmt -w .
# Benchmark (Fiber-specific -- prefork mode)
go build -o bin/server ./cmd/server && PREFORK=true ./bin/server
fasthttp, not net/http. Most net/http-based middleware is incompatible. Use Fiber-specific middleware or adapt via fiber.ConvertRequest/fiber.ConvertResponse (Fiber v2.50+).c.UserContext() instead of c.Context() (which returns *fasthttp.RequestCtx). Pass c.UserContext() to services and repositories.Prefork: true in fiber.Config to spawn one process per CPU core. Useful for CPU-bound workloads. Note: in-memory state is NOT shared across processes.app.Static() for serving assets. Fiber handles this efficiently via fasthttp.app.Test() for integration tests -- it accepts *http.Request and returns *http.Response. No need for a running server.golang:1.22-alpine for build, alpine:latest for runtime. Copy only the binary.github.com/gofiber/websocket/v2 for WebSocket support. It wraps fasthttp/websocket.github.com/gofiber/fiber/v3. Breaking API changes. Use v2 for production. Evaluate v3 for greenfield projects.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.