Scaffold and develop production-ready REST APIs using the Gin web framework with GORM, Swagger, and idiomatic Go patterns.
66
55%
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/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.