Docs / Programming & Development / Go with Gin Framework

Go with Gin Framework

By Admin · Mar 15, 2026 · Updated Apr 24, 2026 · 364 views · 5 min read

Gin is Go's most popular web framework, offering a martini-like API with up to 40x better performance. Its balance of simplicity, performance, and rich middleware ecosystem makes it the go-to choice for building REST APIs in Go. This guide covers building a production-ready API from project setup through deployment.

Project Setup

mkdir gin-api && cd gin-api
go mod init github.com/yourname/gin-api

go get github.com/gin-gonic/gin
go get github.com/gin-contrib/cors
go get github.com/gin-contrib/gzip
go get github.com/joho/godotenv

Application Code

// main.go
package main

import (
    "log"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
    "github.com/gin-contrib/cors"
    "github.com/gin-contrib/gzip"
    "github.com/joho/godotenv"
)

func main() {
    godotenv.Load()

    if os.Getenv("GIN_MODE") == "release" {
        gin.SetMode(gin.ReleaseMode)
    }

    r := gin.New()
    r.Use(gin.Recovery())
    r.Use(gin.Logger())
    r.Use(gzip.Gzip(gzip.DefaultCompression))
    r.Use(cors.Default())

    // Health check
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "healthy"})
    })

    // API v1 group
    v1 := r.Group("/api/v1")
    {
        users := v1.Group("/users")
        {
            users.GET("", listUsers)
            users.GET("/:id", getUser)
            users.POST("", createUser)
            users.PUT("/:id", updateUser)
            users.DELETE("/:id", deleteUser)
        }
    }

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    log.Printf("Starting server on :%s", port)
    r.Run(":" + port)
}

// Request/Response types
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 User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

type ErrorResponse struct {
    Error   string            `json:"error"`
    Details map[string]string `json:"details,omitempty"`
}

func listUsers(c *gin.Context) {
    // Pagination
    page := c.DefaultQuery("page", "1")
    limit := c.DefaultQuery("limit", "20")
    _ = page; _ = limit

    users := []User{
        {ID: 1, Name: "Alice", Email: "alice@example.com", Age: 30},
        {ID: 2, Name: "Bob", Email: "bob@example.com", Age: 25},
    }
    c.JSON(http.StatusOK, gin.H{
        "data":  users,
        "total": len(users),
        "page":  1,
    })
}

func getUser(c *gin.Context) {
    id := c.Param("id")
    c.JSON(http.StatusOK, User{ID: 1, Name: "Alice", Email: "alice@example.com"})
    _ = id
}

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

    user := User{ID: 3, Name: req.Name, Email: req.Email, Age: req.Age}
    c.JSON(http.StatusCreated, user)
}

func updateUser(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"message": "updated"})
}

func deleteUser(c *gin.Context) {
    c.Status(http.StatusNoContent)
}

Custom Middleware

// middleware/auth.go
package middleware

import (
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
)

func AuthRequired() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" || !strings.HasPrefix(token, "Bearer ") {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "Authorization header required",
            })
            return
        }

        tokenStr := strings.TrimPrefix(token, "Bearer ")
        // Validate token...
        userID, err := validateToken(tokenStr)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "Invalid token",
            })
            return
        }

        c.Set("userID", userID)
        c.Next()
    }
}

func RateLimit(limit int) gin.HandlerFunc {
    // Simple in-memory rate limiter
    return func(c *gin.Context) {
        ip := c.ClientIP()
        // Check rate limit for IP...
        _ = ip; _ = limit
        c.Next()
    }
}

Structured Error Handling

// errors/errors.go
package errors

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

type AppError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func (e *AppError) Error() string {
    return e.Message
}

func NotFound(msg string) *AppError {
    return &AppError{Code: http.StatusNotFound, Message: msg}
}

func BadRequest(msg string) *AppError {
    return &AppError{Code: http.StatusBadRequest, Message: msg}
}

// Error handling middleware
func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        if len(c.Errors) > 0 {
            err := c.Errors.Last().Err
            if appErr, ok := err.(*AppError); ok {
                c.JSON(appErr.Code, gin.H{"error": appErr.Message})
            } else {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
            }
        }
    }
}

Database with GORM

go get gorm.io/gorm
go get gorm.io/driver/postgres
// database/db.go
package database

import (
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "log"
    "os"
)

var DB *gorm.DB

func Init() {
    dsn := os.Getenv("DATABASE_URL")
    var err error
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("Database connection failed: %v", err)
    }

    sqlDB, _ := DB.DB()
    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetMaxOpenConns(50)

    // Auto-migrate models
    DB.AutoMigrate(&User{})
}

Build and Deploy

# Build for Linux
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w -X main.version=1.0.0" -o api-server .

# Docker deployment
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server .

FROM scratch
COPY --from=builder /server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]

Testing

// handlers_test.go
package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

func TestHealthEndpoint(t *testing.T) {
    gin.SetMode(gin.TestMode)
    r := gin.New()
    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "healthy"})
    })

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/health", nil)
    r.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.Contains(t, w.Body.String(), "healthy")
}

Summary

Gin is the standard choice for Go web APIs, offering excellent performance with a clean, familiar API. Its validation, middleware, and routing capabilities handle most web application needs out of the box. Deployment follows the standard Go pattern: compile a single binary, deploy with systemd, and reverse proxy with Nginx. The result is a fast, memory-efficient API server that typically handles 50,000-100,000 requests per second on modest VPS hardware.

Was this article helpful?