Scaffold and develop high-performance REST APIs using the Fiber v2 web framework with GORM/SQLX, Express-inspired routing, and idiomatic Go patterns.
73
62%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./backend-go/fiber-project-starter/SKILL.mdScaffold 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@latestmyapp/
├── 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
└── Makefilefasthttp 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/serverfasthttp, 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.181fcbc
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.