Scaffold and develop production-ready REST APIs using the Gin web framework with GORM, Swagger, and idiomatic Go patterns.
52
55%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No known issues
Fix and improve this skill with Tessl
tessl review fix ./backend-go/gin-project-starter/SKILL.mdScaffold and develop production-ready REST APIs using the Gin web framework with GORM, Swagger, and idiomatic Go patterns.
go version)swag CLI for Swagger generation (go install github.com/swaggo/swag/cmd/swag@latest)golangci-lint for linting (go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest)mkdir -p myapp && cd myapp
go mod init github.com/yourorg/myapp
go get github.com/gin-gonic/gin@v1.10.0
go get gorm.io/gorm@latest
go get gorm.io/driver/postgres@latest
go get github.com/swaggo/gin-swagger@latest
go get github.com/swaggo/files@latest
go get github.com/gin-contrib/cors@latestmyapp/
├── cmd/
│ └── server/
│ └── main.go # Entrypoint, wires everything together
├── internal/
│ ├── config/
│ │ └── config.go # Env-based config loading
│ ├── handler/
│ │ ├── user.go # HTTP handlers grouped by domain
│ │ └── health.go
│ ├── middleware/
│ │ ├── auth.go # JWT/bearer token auth middleware
│ │ └── ratelimit.go
│ ├── model/
│ │ └── user.go # GORM models
│ ├── repository/
│ │ └── user.go # DB access layer
│ ├── service/
│ │ └── user.go # Business logic
│ └── router/
│ └── router.go # Route definitions and middleware registration
├── docs/ # Generated by swag init
├── .env.example # Environment variable template
├── go.mod
├── go.sum
└── Makefileinternal/ to prevent external imports of application internals.user.go, order.go).gorm.DB directly in handlers. Always go through repository and service layers.context.Context propagation from c.Request.Context() through service and repository calls.binding:"required", binding:"email".os.Getenv or a config library -- never hardcode credentials.// cmd/server/main.go
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/yourorg/myapp/internal/config"
"github.com/yourorg/myapp/internal/router"
)
// @title MyApp API
// @version 1.0
// @description A sample API server.
// @host localhost:8080
// @BasePath /api/v1
func main() {
cfg := config.Load()
r := router.Setup(cfg)
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
slog.Info("server starting", "port", cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("listen failed", "error", err)
os.Exit(1)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("forced shutdown", "error", err)
}
slog.Info("server exited")
}// internal/router/router.go
package router
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"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"
"gorm.io/gorm"
)
func Setup(cfg *config.Config, db *gorm.DB) *gin.Engine {
if cfg.Env == "production" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
// Global middleware
r.Use(gin.Recovery())
r.Use(gin.Logger())
r.Use(cors.New(cors.Config{
AllowOrigins: cfg.AllowedOrigins,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
}))
// Swagger
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// Health check (no auth)
r.GET("/healthz", handler.Healthz)
// Wire dependencies
userRepo := repository.NewUserRepository(db)
userSvc := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userSvc)
// API v1 group
v1 := r.Group("/api/v1")
{
// Public routes
auth := v1.Group("/auth")
{
auth.POST("/register", userHandler.Register)
}
// Protected routes
protected := v1.Group("/")
protected.Use(middleware.AuthRequired(cfg.JWTSecret))
{
users := protected.Group("/users")
{
users.GET("", userHandler.ListUsers)
users.GET("/:id", userHandler.GetUser)
users.PUT("/:id", userHandler.UpdateUser)
users.DELETE("/:id", userHandler.DeleteUser)
}
}
}
return r
}// internal/handler/user.go
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/yourorg/myapp/internal/service"
)
// UserHandler groups all user-related HTTP handlers.
// Dependencies are injected via the constructor.
type UserHandler struct {
svc service.UserService
}
func NewUserHandler(svc service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2,max=100"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"required,gte=1,lte=150"`
}
type UserResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// Register godoc
// @Summary Register a new user
// @Tags auth
// @Accept json
// @Produce json
// @Param body body CreateUserRequest true "User data"
// @Success 201 {object} UserResponse
// @Failure 400 {object} ErrorResponse
// @Router /auth/register [post]
func (h *UserHandler) Register(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
ctx := c.Request.Context()
user, err := h.svc.Create(ctx, req.Name, req.Email, req.Age)
if err != nil {
if errors.Is(err, service.ErrDuplicateEmail) {
c.JSON(http.StatusConflict, ErrorResponse{Error: "email already registered"})
return
}
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal error"})
return
}
c.JSON(http.StatusCreated, UserResponse{
ID: user.ID,
Name: user.Name,
Email: user.Email,
})
}
// GetUser godoc
// @Summary Get user by ID
// @Tags users
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} UserResponse
// @Failure 404 {object} ErrorResponse
// @Router /users/{id} [get]
func (h *UserHandler) GetUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "invalid id"})
return
}
ctx := c.Request.Context()
user, err := h.svc.GetByID(ctx, uint(id))
if err != nil {
if errors.Is(err, service.ErrNotFound) {
c.JSON(http.StatusNotFound, ErrorResponse{Error: "user not found"})
return
}
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal error"})
return
}
c.JSON(http.StatusOK, UserResponse{
ID: user.ID,
Name: user.Name,
Email: user.Email,
})
}
// ListUsers with query params for pagination
func (h *UserHandler) ListUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
ctx := c.Request.Context()
users, total, err := h.svc.List(ctx, page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal error"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": users,
"total": total,
"page": page,
"page_size": pageSize,
})
}
type ErrorResponse struct {
Error string `json:"error"`
}// internal/middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func AuthRequired(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
header := c.GetHeader("Authorization")
if header == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
return
}
parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization format"})
return
}
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 {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid claims"})
return
}
c.Set("userID", claims["sub"])
c.Next()
}
}// internal/model/user.go
package model
import "gorm.io/gorm"
type User struct {
gorm.Model
Name string `gorm:"type:varchar(100);not null"`
Email string `gorm:"type:varchar(255);uniqueIndex;not null"`
Age int `gorm:"not null"`
}// internal/repository/user.go
package repository
import (
"context"
"errors"
"gorm.io/gorm"
"github.com/yourorg/myapp/internal/model"
)
type UserRepository interface {
Create(ctx context.Context, user *model.User) error
GetByID(ctx context.Context, id uint) (*model.User, error)
GetByEmail(ctx context.Context, email string) (*model.User, error)
List(ctx context.Context, offset, limit int) ([]model.User, int64, error)
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Create(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *userRepository) GetByID(ctx context.Context, id uint) (*model.User, error) {
var user model.User
err := r.db.WithContext(ctx).First(&user, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return &user, err
}
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) {
var user model.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return &user, err
}
func (r *userRepository) List(ctx context.Context, offset, limit int) ([]model.User, int64, error) {
var users []model.User
var total int64
if err := r.db.WithContext(ctx).Model(&model.User{}).Count(&total).Error; err != nil {
return nil, 0, err
}
err := r.db.WithContext(ctx).Offset(offset).Limit(limit).Find(&users).Error
return users, total, err
}
var ErrNotFound = errors.New("record not found")// internal/config/config.go
package config
import "os"
type Config struct {
Port string
Env string
DatabaseURL string
JWTSecret string
AllowedOrigins []string
}
func Load() *Config {
return &Config{
Port: getEnv("PORT", "8080"),
Env: getEnv("APP_ENV", "development"),
DatabaseURL: getEnv("DATABASE_URL", "postgres://localhost:5432/myapp?sslmode=disable"),
JWTSecret: getEnv("JWT_SECRET", "change-me-in-production"),
AllowedOrigins: []string{getEnv("CORS_ORIGIN", "http://localhost:3000")},
}
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}.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 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 (run from project root)
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 .golang-migrate or use GORM's AutoMigrate for dev only. For production, use versioned migrations via github.com/golang-migrate/migrate/v4.golang:1.22-alpine for build, alpine:latest for runtime. Copy only the binary.prometheus/client_golang for metrics, gin.Logger() writes to stdout which pairs with structured log collectors.httptest.NewRecorder() with gin.CreateTestContext() for handler tests. Mock repository interfaces for service tests.golangci-lint run, go test -race ./..., and go vet ./... in CI pipeline.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.