CtrlK
BlogDocsLog inGet started
Tessl Logo

fiber-project-starter

Scaffold and develop high-performance REST APIs using the Fiber v2 web framework with GORM/SQLX, Express-inspired routing, and idiomatic Go patterns.

73

Quality

62%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./backend-go/fiber-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Fiber Project Starter

Scaffold and develop high-performance REST APIs using the Fiber v2 web framework with GORM/SQLX, Express-inspired routing, and idiomatic Go patterns.

Prerequisites

  • Go 1.22+ installed (go version)
  • PostgreSQL or MySQL for production (SQLite for local dev)
  • 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)

Scaffold Command

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

Project Structure

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

Key Conventions

  • Fiber uses 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.
  • Fiber's Ctx is pooled and reused. Never store *fiber.Ctx references beyond the handler lifecycle. Copy values you need.
  • One handler file per domain entity. Handlers receive service interfaces via struct fields.
  • Use c.BodyParser() for JSON/form binding, c.QueryParser() for query params, c.ParamsInt() for URL params.
  • Register a custom validator separately since Fiber does not have built-in validation.
  • Group routes with app.Group() and apply middleware per group.
  • Return errors via c.Status(code).JSON(...) -- Fiber handlers return error but the error is for framework-level failures, not HTTP error responses.
  • Use fiber.Map as a shorthand for map[string]interface{} in JSON responses.

Essential Patterns

Main entrypoint with graceful shutdown

// 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")
}

Router setup with middleware and route groups

// 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,
	})
}

Handler with request parsing and validation

// 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"})
}

Auth middleware for Fiber

// 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()
	}
}

Database connection with GORM

// 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
}

SQLX alternative for database access

// 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
}

First Steps After Scaffold

  1. Copy .env.example to .env and fill in values
  2. Install dependencies: go mod download
  3. Run database migrations: migrate -path migrations -database "$DATABASE_URL" up
  4. Start dev server: go run ./cmd/server
  5. Test the API: curl http://localhost:8080/healthz

Common Commands

# 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

Integration Notes

  • fasthttp caveat: Fiber uses 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+).
  • Context propagation: Use c.UserContext() instead of c.Context() (which returns *fasthttp.RequestCtx). Pass c.UserContext() to services and repositories.
  • Prefork mode: Set 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.
  • Static files: Use app.Static() for serving assets. Fiber handles this efficiently via fasthttp.
  • Testing: Use app.Test() for integration tests -- it accepts *http.Request and returns *http.Response. No need for a running server.
  • Docker: Multi-stage build with golang:1.22-alpine for build, alpine:latest for runtime. Copy only the binary.
  • Websockets: Use github.com/gofiber/websocket/v2 for WebSocket support. It wraps fasthttp/websocket.
  • Fiber v3 beta: Available as github.com/gofiber/fiber/v3. Breaking API changes. Use v2 for production. Evaluate v3 for greenfield projects.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.