Routing, middleware, request binding, JSON responses, auth, database & deployment patterns
Framework / Backend// Install
// go get -u github.com/gin-gonic/gin
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default() // includes Logger + Recovery middleware
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run(":8080") // listen on 0.0.0.0:8080
}
// gin.H is a shortcut for map[string]any// HTTP methods
r.GET("/users", getUsers)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.PATCH("/users/:id", patchUser)
r.DELETE("/users/:id", deleteUser)
// Route groups (shared prefix + middleware)
api := r.Group("/api/v1")
{
api.GET("/users", getUsers)
api.POST("/users", createUser)
// Nested group with auth middleware
admin := api.Group("/admin")
admin.Use(AuthMiddleware())
{
admin.GET("/stats", getStats)
admin.DELETE("/users/:id", deleteUser)
}
}
// Static files
r.Static("/assets", "./public")
r.StaticFile("/favicon.ico", "./public/favicon.ico")
// No route (404 handler)
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
})// Path parameters
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id") // string
c.JSON(200, gin.H{"id": id})
})
// Wildcard
r.GET("/files/*filepath", func(c *gin.Context) {
path := c.Param("filepath") // e.g., "/docs/readme.md"
})
// Query parameters
// GET /search?q=golang&page=2&limit=10
r.GET("/search", func(c *gin.Context) {
q := c.Query("q") // "golang"
page := c.DefaultQuery("page", "1") // "2"
limit := c.DefaultQuery("limit", "20") // "10"
})
// Headers
token := c.GetHeader("Authorization")
c.Header("X-Request-ID", "abc123")type CreateUserInput 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"`
}
r.POST("/users", func(c *gin.Context) {
var input CreateUserInput
// ShouldBindJSON β returns error, doesn't abort
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"user": input})
})
// Bind URI params
type UserURI struct {
ID int `uri:"id" binding:"required"`
}
r.GET("/users/:id", func(c *gin.Context) {
var uri UserURI
c.ShouldBindUri(&uri)
})
// Bind query params
type SearchQuery struct {
Q string `form:"q"`
Page int `form:"page,default=1"`
Limit int `form:"limit,default=20"`
}
r.GET("/search", func(c *gin.Context) {
var q SearchQuery
c.ShouldBindQuery(&q)
})// JSON
c.JSON(http.StatusOK, gin.H{"message": "success"})
c.JSON(200, user) // struct serialization
// IndentedJSON (pretty-printed)
c.IndentedJSON(200, data)
// String
c.String(200, "Hello %s", name)
// Status only
c.Status(http.StatusNoContent) // 204
// Redirect
c.Redirect(http.StatusMovedPermanently, "https://example.com")
// File download
c.File("./files/report.pdf")
c.FileAttachment("./files/report.pdf", "report.pdf")
// Streaming
c.Stream(func(w io.Writer) bool {
fmt.Fprintf(w, "data: %s\n\n", msg)
return keepStreaming
})// Custom middleware
func RequestLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next() // process request
latency := time.Since(start)
status := c.Writer.Status()
log.Printf("[%d] %s %s (%v)",
status, c.Request.Method, c.Request.URL.Path, latency)
}
}
// Apply globally
r.Use(RequestLogger())
// CORS middleware
import "github.com/gin-contrib/cors"
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// Rate limiter
import "github.com/gin-contrib/ratelimit"
// Recovery (built-in) β recovers from panics
r.Use(gin.Recovery())import "github.com/golang-jwt/jwt/v5"
var jwtSecret = []byte(os.Getenv("JWT_SECRET"))
// Generate token
func GenerateToken(userID uint) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(24 * time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// Auth middleware
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
header := c.GetHeader("Authorization")
if header == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "missing token"})
return
}
tokenStr := strings.TrimPrefix(header, "Bearer ")
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
return jwtSecret, nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
return
}
claims := token.Claims.(jwt.MapClaims)
c.Set("user_id", claims["user_id"])
c.Next()
}
}
// Use in handler
userID := c.MustGet("user_id").(float64)// Common validation tags (uses go-playground/validator)
type Input struct {
Name string `binding:"required"`
Email string `binding:"required,email"`
Age int `binding:"gte=0,lte=150"`
Password string `binding:"required,min=8,max=64"`
URL string `binding:"url"`
Role string `binding:"oneof=admin user moderator"`
Tags []string `binding:"dive,min=1,max=50"`
}
// Custom validator
import "github.com/go-playground/validator/v10"
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("nowhitespace", func(fl validator.FieldLevel) bool {
return !strings.Contains(fl.Field().String(), " ")
})
}import (
"gorm.io/gorm"
"gorm.io/driver/postgres"
)
type User struct {
gorm.Model // ID, CreatedAt, UpdatedAt, DeletedAt
Name string `json:"name"`
Email string `json:"email" gorm:"uniqueIndex"`
Posts []Post `json:"posts"`
}
type Post struct {
gorm.Model
Title string `json:"title"`
Content string `json:"content"`
UserID uint `json:"user_id"`
}
// Connect
dsn := "host=localhost user=dev password=pass dbname=app port=5432"
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db.AutoMigrate(&User{}, &Post{})
// CRUD in handlers
r.GET("/users", func(c *gin.Context) {
var users []User
db.Preload("Posts").Find(&users)
c.JSON(200, users)
})
r.POST("/users", func(c *gin.Context) {
var user User
c.ShouldBindJSON(&user)
result := db.Create(&user)
if result.Error != nil {
c.JSON(400, gin.H{"error": result.Error.Error()})
return
}
c.JSON(201, user)
})// Standardized error response
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last()
c.JSON(c.Writer.Status(), APIError{
Code: c.Writer.Status(),
Message: err.Error(),
})
}
}
}
// Usage in handler
r.GET("/users/:id", func(c *gin.Context) {
user, err := findUser(c.Param("id"))
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": "user not found"})
return
}
c.JSON(200, user)
})import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func setupRouter() *gin.Engine {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
return r
}
func TestPing(t *testing.T) {
r := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "pong")
}
// Test with JSON body
func TestCreateUser(t *testing.T) {
r := setupRouter()
body := strings.NewReader(`{"name":"Alice","email":"a@b.com"}`)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/users", body)
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
}# Multi-stage Dockerfile
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
# Set Gin to release mode in production
# export GIN_MODE=release