Skip to main content

Gin Framework: Routing, Middleware, and Handlers

Created: May 8, 2026 Larry Qu 6 min read

Introduction

Gin is a lightweight, high-performance web framework for Go. It provides a simple API for building web applications with excellent routing, middleware support, and request handling. This guide covers routing, middleware, handlers, and practical patterns for building production-ready applications with Gin. See Go Installation Guide, Go Ecosystem Overview, Go Best Practices for more context.

Core Concepts

What is Gin?

Gin is a web framework that:

  • Provides fast HTTP routing
  • Supports middleware chains
  • Offers built-in validation
  • Includes JSON binding
  • Has excellent error handling

Key Components

  1. Router: Maps HTTP requests to handlers
  2. Middleware: Processes requests before handlers
  3. Handlers: Functions that process requests
  4. Context: Request/response data container

Good: Basic Routing

Setting Up Gin

package main

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

// ✅ GOOD: Basic Gin setup
func main() {
	// Create router
	router := gin.Default()

	// Define routes
	router.GET("/", handleHome)
	router.GET("/users/:id", handleGetUser)
	router.POST("/users", handleCreateUser)
	router.PUT("/users/:id", handleUpdateUser)
	router.DELETE("/users/:id", handleDeleteUser)

	// Start server
	router.Run(":8080")
}

func handleHome(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "Welcome to Gin",
	})
}

func handleGetUser(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"id": id,
	})
}

func handleCreateUser(c *gin.Context) {
	var user struct {
		Name  string `json:"name"`
		Email string `json:"email"`
	}

	if err := c.ShouldBindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusCreated, user)
}

func handleUpdateUser(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"id":      id,
		"updated": true,
	})
}

func handleDeleteUser(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"id":      id,
		"deleted": true,
	})
}

Route Groups

package main

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

// ✅ GOOD: Route grouping
func setupRoutes(router *gin.Engine) {
	// Public routes
	public := router.Group("/api/v1")
	{
		public.GET("/health", handleHealth)
		public.POST("/login", handleLogin)
	}

	// Protected routes
	protected := router.Group("/api/v1")
	protected.Use(authMiddleware())
	{
		protected.GET("/users", handleListUsers)
		protected.GET("/users/:id", handleGetUser)
		protected.POST("/users", handleCreateUser)
	}

	// Admin routes
	admin := router.Group("/api/v1/admin")
	admin.Use(authMiddleware(), adminMiddleware())
	{
		admin.GET("/stats", handleStats)
		admin.DELETE("/users/:id", handleDeleteUser)
	}
}

func handleHealth(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"status": "ok"})
}

func handleLogin(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"token": "jwt-token"})
}

func handleListUsers(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"users": []string{}})
}

func handleGetUser(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"id": c.Param("id")})
}

func handleCreateUser(c *gin.Context) {
	c.JSON(http.StatusCreated, gin.H{"created": true})
}

func handleDeleteUser(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"deleted": true})
}

func handleStats(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"stats": "data"})
}

func authMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// Check auth
		c.Next()
	}
}

func adminMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// Check admin role
		c.Next()
	}
}

Good: Middleware

Creating Middleware

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
	"time"
)

// ✅ GOOD: Logging middleware
func loggingMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()

		// Process request
		c.Next()

		// Log after request
		duration := time.Since(start)
		log.Printf("%s %s %d %v",
			c.Request.Method,
			c.Request.URL.Path,
			c.Writer.Status(),
			duration,
		)
	}
}

// ✅ GOOD: Error handling middleware
func errorHandlingMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("Panic: %v", err)
				c.JSON(500, gin.H{"error": "Internal server error"})
			}
		}()
		c.Next()
	}
}

// ✅ GOOD: CORS middleware
func corsMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
		c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type")

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(204)
			return
		}

		c.Next()
	}
}

// ✅ GOOD: Rate limiting middleware
func rateLimitMiddleware(limit int) gin.HandlerFunc {
	return func(c *gin.Context) {
		// Implement rate limiting logic
		c.Next()
	}
}

// ✅ GOOD: Using middleware
func setupMiddleware(router *gin.Engine) {
	router.Use(loggingMiddleware())
	router.Use(errorHandlingMiddleware())
	router.Use(corsMiddleware())
}

Good: Handlers and Context

Working with Context

package main

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

// ✅ GOOD: Extract query parameters
func handleSearch(c *gin.Context) {
	query := c.Query("q")
	limit := c.DefaultQuery("limit", "10")

	c.JSON(http.StatusOK, gin.H{
		"query": query,
		"limit": limit,
	})
}

// ✅ GOOD: Extract form data
func handleForm(c *gin.Context) {
	name := c.PostForm("name")
	email := c.PostForm("email")

	c.JSON(http.StatusOK, gin.H{
		"name":  name,
		"email": email,
	})
}

// ✅ GOOD: Bind JSON
func handleJSON(c *gin.Context) {
	var user struct {
		Name  string `json:"name" binding:"required"`
		Email string `json:"email" binding:"required,email"`
		Age   int    `json:"age" binding:"min=0,max=150"`
	}

	if err := c.ShouldBindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, user)
}

// ✅ GOOD: Set response headers
func handleHeaders(c *gin.Context) {
	c.Header("X-Custom-Header", "value")
	c.JSON(http.StatusOK, gin.H{"message": "ok"})
}

// ✅ GOOD: Set cookies
func handleCookies(c *gin.Context) {
	c.SetCookie("session", "abc123", 3600, "/", "localhost", false, true)
	c.JSON(http.StatusOK, gin.H{"message": "cookie set"})
}

// ✅ GOOD: Store data in context
func handleContextData(c *gin.Context) {
	c.Set("user_id", 123)
	c.Set("role", "admin")

	// Retrieve in middleware
	userID, _ := c.Get("user_id")
	c.JSON(http.StatusOK, gin.H{"user_id": userID})
}

Advanced Patterns

Custom Error Handling

package main

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

// ✅ GOOD: Custom error response
type ErrorResponse struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	Details string `json:"details,omitempty"`
}

func handleError(c *gin.Context, statusCode int, message string, details string) {
	c.JSON(statusCode, ErrorResponse{
		Code:    statusCode,
		Message: message,
		Details: details,
	})
}

// ✅ GOOD: Error handling in handlers
func handleUserWithError(c *gin.Context) {
	id := c.Param("id")

	// Validate ID
	if id == "" {
		handleError(c, http.StatusBadRequest, "Invalid user ID", "")
		return
	}

	// Fetch user
	user, err := getUser(id)
	if err != nil {
		handleError(c, http.StatusNotFound, "User not found", err.Error())
		return
	}

	c.JSON(http.StatusOK, user)
}

func getUser(id string) (interface{}, error) {
	return nil, nil
}

File Upload Handling

package main

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

// ✅ GOOD: Handle file upload
func handleFileUpload(c *gin.Context) {
	file, err := c.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "No file"})
		return
	}

	// Save file
	if err := c.SaveUploadedFile(file, "./uploads/"+file.Filename); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Upload failed"})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"filename": file.Filename,
		"size":     file.Size,
	})
}

// ✅ GOOD: Handle multiple files
func handleMultipleFiles(c *gin.Context) {
	form, _ := c.MultipartForm()
	files := form.File["files"]

	for _, file := range files {
		c.SaveUploadedFile(file, "./uploads/"+file.Filename)
	}

	c.JSON(http.StatusOK, gin.H{
		"count": len(files),
	})
}

Best Practices

1. Use Route Groups

// ✅ GOOD: Organize routes with groups
api := router.Group("/api/v1")
{
	api.GET("/users", listUsers)
	api.POST("/users", createUser)
}

// ❌ BAD: Flat route structure
router.GET("/api/v1/users", listUsers)
router.POST("/api/v1/users", createUser)

2. Validate Input

// ✅ GOOD: Validate with binding tags
type User struct {
	Name  string `json:"name" binding:"required,min=3,max=50"`
	Email string `json:"email" binding:"required,email"`
}

// ❌ BAD: No validation
type BadUser struct {
	Name  string
	Email string
}

3. Use Middleware for Cross-Cutting Concerns

// ✅ GOOD: Middleware for auth
router.Use(authMiddleware())

// ❌ BAD: Auth in every handler
func handler(c *gin.Context) {
	// Check auth
	// Do work
}

Resources

Summary

Gin is a powerful, lightweight web framework for Go. Use route groups to organize endpoints, middleware for cross-cutting concerns, and proper error handling for robust applications. Gin’s simplicity and performance make it ideal for building REST APIs and web services.

Comments

Share this article

Scan to read on mobile

👍 Was this article helpful?