gin

SKILL.md

Gin Framework Guide

Applies to: Gin 1.9+, REST APIs, Microservices, Web Applications Language Guide: @.claude/skills/go-guide/SKILL.md

Overview

Gin is a high-performance HTTP web framework written in Go featuring a martini-like API with performance up to 40x faster. It is the most popular Go web framework, ideal for building REST APIs and microservices.

Use Gin when:

  • Building high-performance REST APIs
  • You need a mature, well-documented framework
  • Middleware ecosystem is important
  • You want a balanced approach (not too minimal, not too heavy)

Consider alternatives when:

  • You need maximum minimalism (use standard library)
  • You want built-in WebSocket support (use Fiber)
  • You prefer a different API style (use Echo)

Guardrails

Gin-Specific Rules

  • Use application factory pattern for testability
  • Group routes with versioning (/api/v1)
  • Use middleware for cross-cutting concerns (auth, logging, CORS)
  • Use gin.Context for request-scoped data only
  • Use binding tags for input validation
  • Return consistent JSON response structure across all endpoints
  • Use proper HTTP status codes
  • Set gin.ReleaseMode in production
  • Configure proper server timeouts (read, write, idle)
  • Implement graceful shutdown for all servers
  • Use connection pooling for database access
  • Use pagination for all list endpoints

Anti-Patterns

  • Do not use gin.Default() in production without understanding its middleware
  • Do not store business logic in handlers (use service layer)
  • Do not return raw error messages to clients
  • Do not skip input validation on any endpoint
  • Do not use global state; use dependency injection

Project Structure

myproject/
├── cmd/
│   └── api/
│       └── main.go           # Entry point, server setup, graceful shutdown
├── internal/
│   ├── config/
│   │   └── config.go         # Configuration from env vars
│   ├── handler/
│   │   ├── handler.go        # Handler registry struct
│   │   ├── user.go           # User handlers
│   │   └── auth.go           # Auth handlers
│   ├── middleware/
│   │   ├── auth.go           # JWT/Bearer auth middleware
│   │   ├── cors.go           # CORS middleware
│   │   └── logger.go         # Request logging middleware
│   ├── model/
│   │   ├── user.go           # Domain model + request/response DTOs
│   │   └── response.go       # Standardized response wrappers
│   ├── repository/
│   │   ├── repository.go     # Repository registry (interfaces)
│   │   └── user.go           # User repository implementation
│   ├── service/
│   │   ├── service.go        # Service registry (interfaces)
│   │   └── user.go           # User business logic
│   └── router/
│       └── router.go         # Route definitions and grouping
├── pkg/
│   ├── validator/
│   │   └── validator.go      # Custom validators
│   └── response/
│       └── response.go       # Shared response helpers
├── migrations/
├── .env.example
├── go.mod
├── go.sum
├── Makefile
└── README.md

Layer responsibilities:

  • handler/ — HTTP concerns only: parse request, call service, write response
  • service/ — Business logic, validation, orchestration
  • repository/ — Data access, database queries
  • model/ — Domain types, request/response DTOs, validation tags
  • middleware/ — Cross-cutting: auth, logging, CORS, rate limiting
  • router/ — Route registration, grouping, middleware attachment

Routing

Route Groups and Versioning

func Setup(handlers *handler.Handlers, mw *middleware.Middleware) *gin.Engine {
    r := gin.New()

    // Global middleware
    r.Use(gin.Recovery())
    r.Use(middleware.Logger())
    r.Use(middleware.CORS())

    // Health check (always public)
    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "ok"})
    })

    // API v1 routes
    v1 := r.Group("/api/v1")
    {
        // Public routes
        auth := v1.Group("/auth")
        {
            auth.POST("/login", handlers.Auth.Login)
            auth.POST("/refresh", handlers.Auth.Refresh)
        }

        // Protected routes
        users := v1.Group("/users")
        {
            users.POST("", handlers.User.CreateUser)        // Public
            users.Use(mw.Auth())                             // Auth from here down
            users.GET("", handlers.User.GetUsers)
            users.GET("/me", handlers.User.GetCurrentUser)
            users.GET("/:id", handlers.User.GetUser)
            users.PATCH("/:id", handlers.User.UpdateUser)
            users.DELETE("/:id", mw.AdminOnly(), handlers.User.DeleteUser)
        }
    }

    return r
}

Routing conventions:

  • Always use gin.New() (not gin.Default()) and add middleware explicitly
  • Group public and protected routes separately
  • Apply auth middleware at the group level, not per-route
  • Use per-route middleware for fine-grained access (e.g., mw.AdminOnly())
  • Always include a /health endpoint

Middleware

Auth Middleware (JWT Bearer)

func (m *Middleware) Auth() gin.HandlerFunc {
    return func(c *gin.Context) {
        header := c.GetHeader("Authorization")
        if header == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized,
                model.NewErrorResponse("missing authorization header"))
            return
        }

        parts := strings.Split(header, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.AbortWithStatusJSON(http.StatusUnauthorized,
                model.NewErrorResponse("invalid authorization header"))
            return
        }

        claims, err := m.authService.ValidateToken(parts[1])
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized,
                model.NewErrorResponse("invalid token"))
            return
        }

        // Set user context for downstream handlers
        c.Set("user_id", claims.UserID)
        c.Set("is_admin", claims.IsAdmin)
        c.Next()
    }
}

Role-Based Access

func (m *Middleware) AdminOnly() gin.HandlerFunc {
    return func(c *gin.Context) {
        isAdmin, exists := c.Get("is_admin")
        if !exists || !isAdmin.(bool) {
            c.AbortWithStatusJSON(http.StatusForbidden,
                model.NewErrorResponse("admin access required"))
            return
        }
        c.Next()
    }
}

Request Logger

func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        query := c.Request.URL.RawQuery

        c.Next()

        if query != "" {
            path = path + "?" + query
        }

        log.Printf("[GIN] %3d | %13v | %15s | %-7s %s",
            c.Writer.Status(), time.Since(start),
            c.ClientIP(), c.Request.Method, path)
    }
}

Request Binding and Validation

Binding Tags

Gin uses binding struct tags for request validation (backed by go-playground/validator).

// Create request — all fields required
type CreateUserRequest struct {
    Email     string `json:"email" binding:"required,email"`
    Password  string `json:"password" binding:"required,min=8"`
    FirstName string `json:"first_name" binding:"required,min=1,max=100"`
    LastName  string `json:"last_name" binding:"required,min=1,max=100"`
}

// Update request — all fields optional (pointer types)
type UpdateUserRequest struct {
    FirstName *string `json:"first_name" binding:"omitempty,min=1,max=100"`
    LastName  *string `json:"last_name" binding:"omitempty,min=1,max=100"`
    IsActive  *bool   `json:"is_active"`
}

Handler Binding Pattern

func (h *UserHandler) CreateUser(c *gin.Context) {
    var req model.CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, model.NewErrorResponse(err.Error()))
        return
    }

    user, err := h.userService.Create(c.Request.Context(), &req)
    if err != nil {
        handleServiceError(c, err)
        return
    }

    c.JSON(http.StatusCreated, model.NewSuccessResponse(user.ToResponse()))
}

Binding conventions:

  • Use ShouldBindJSON (not BindJSON) to control error responses yourself
  • Use pointer fields for optional/partial update DTOs
  • Always validate before passing to service layer
  • Separate request DTOs from domain models

Query Parameter Binding

func (h *UserHandler) GetUsers(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))

    users, total, err := h.userService.GetAll(c.Request.Context(), page, perPage)
    if err != nil {
        c.JSON(http.StatusInternalServerError, model.NewErrorResponse(err.Error()))
        return
    }

    responses := make([]*model.UserResponse, len(users))
    for i, user := range users {
        responses[i] = user.ToResponse()
    }

    c.JSON(http.StatusOK, model.NewPaginatedResponse(responses, page, perPage, total))
}

Error Handling

Standardized Response Models

type Response struct {
    Success bool        `json:"success"`
    Message string      `json:"message,omitempty"`
    Data    interface{} `json:"data,omitempty"`
    Error   string      `json:"error,omitempty"`
}

type PaginatedResponse struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data"`
    Meta    *PageMeta   `json:"meta"`
}

type PageMeta struct {
    Page       int   `json:"page"`
    PerPage    int   `json:"per_page"`
    Total      int64 `json:"total"`
    TotalPages int   `json:"total_pages"`
}

func NewSuccessResponse(data interface{}) *Response {
    return &Response{Success: true, Data: data}
}

func NewErrorResponse(message string) *Response {
    return &Response{Success: false, Error: message}
}

Service Error Mapping

Define domain errors in the service layer, map them to HTTP status in handlers:

// service layer
var (
    ErrUserNotFound      = errors.New("user not found")
    ErrUserAlreadyExists = errors.New("user already exists")
    ErrInvalidCredentials = errors.New("invalid credentials")
)

// handler helper
func handleServiceError(c *gin.Context, err error) {
    switch {
    case errors.Is(err, service.ErrUserNotFound):
        c.JSON(http.StatusNotFound, model.NewErrorResponse(err.Error()))
    case errors.Is(err, service.ErrUserAlreadyExists):
        c.JSON(http.StatusConflict, model.NewErrorResponse(err.Error()))
    case errors.Is(err, service.ErrInvalidCredentials):
        c.JSON(http.StatusUnauthorized, model.NewErrorResponse(err.Error()))
    default:
        c.JSON(http.StatusInternalServerError, model.NewErrorResponse("internal server error"))
    }
}

Error handling rules:

  • Never expose internal errors to clients in production
  • Map domain errors to appropriate HTTP status codes
  • Use errors.Is for sentinel errors, errors.As for typed errors
  • Always return the standardized Response structure

Application Setup

Server with Graceful Shutdown

func main() {
    cfg, err := config.Load()
    if err != nil {
        log.Fatalf("Failed to load config: %v", err)
    }

    if cfg.Environment == "production" {
        gin.SetMode(gin.ReleaseMode)
    }

    // Wire layers: repo -> service -> handler
    db, err := initDB(cfg.DatabaseURL)
    if err != nil {
        log.Fatalf("Failed to connect to database: %v", err)
    }

    repos := repository.NewRepositories(db)
    services := service.NewServices(repos, cfg)
    handlers := handler.NewHandlers(services)
    r := router.Setup(handlers, middleware.NewMiddleware(cfg))

    srv := &http.Server{
        Addr:         ":" + cfg.Port,
        Handler:      r,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server failed: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }
}

Commands Reference

# Initialize project
go mod init myproject

# Install dependencies
go mod tidy

# Run development server
go run cmd/api/main.go

# Build binary
go build -o bin/api cmd/api/main.go

# Run tests
go test ./...
go test -v -cover ./...

# Run with race detection
go test -race ./...

# Lint
golangci-lint run

# Generate Swagger docs (with swaggo)
swag init -g cmd/api/main.go

# Database migrations (using golang-migrate)
migrate -path migrations -database "$DATABASE_URL" up
migrate -path migrations -database "$DATABASE_URL" down

Dependencies

// Core
github.com/gin-gonic/gin v1.9.1

// Auth
github.com/golang-jwt/jwt/v5 v5.0.0
golang.org/x/crypto v0.14.0

// Database
gorm.io/gorm v1.25.5
gorm.io/driver/postgres v1.5.4

// Config
github.com/joho/godotenv v1.5.1

// Testing
github.com/stretchr/testify v1.8.4

// Docs
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.2

Advanced Topics

For detailed patterns and full implementation examples, see:

  • references/patterns.md -- Handler implementations, database integration, authentication service, testing patterns, performance tuning

External References

Weekly Installs
6
Repository
ar4mirez/samuel
GitHub Stars
3
First Seen
Feb 28, 2026
Installed on
opencode6
gemini-cli6
github-copilot6
codex6
kimi-cli6
amp6