How to Use Middleware in Gin

Web
Register middleware in Gin using the Use method on your router or pass it directly to specific route handlers to execute logic before and after request processing.

Middleware in Gin

You are building an API with Gin. You have ten routes. Suddenly, you need to log every request. You copy-paste the logging code into ten handlers. Then you need authentication. You copy-paste that too. Your handlers are bloated. Your code is a mess of duplication. Middleware exists to stop this pattern. It lets you wrap behavior around routes without touching the route logic.

Middleware is a function that runs before or after your route handler. Gin chains these functions together. The request flows through the chain. Each middleware can inspect the request, modify it, reject it, or let it pass. The structure gives you a clean separation between cross-cutting concerns and business logic.

Think of middleware as a series of checkpoints. A request enters the first checkpoint. The checkpoint inspects the request. If it passes, the checkpoint hands the request to the next one. This continues until the request reaches the route handler. The handler processes the request and produces a response. The response then travels back through the checkpoints in reverse order. This onion model lets you add behavior at the edges without cluttering the center.

The core mechanism

Gin middleware is a function with the signature func(*gin.Context). The gin.Context object carries the request, the response, and state shared across the chain. The critical method is c.Next(). This call passes control to the next handler in the chain. Code before c.Next() runs before the downstream handler. Code after c.Next() runs after the downstream handler returns.

Here's the simplest middleware: spawn one, send a message, close the channel.

package main

import (
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	// gin.Default() creates a router with recovery and logger middleware enabled
	r := gin.Default()

	// Use registers a middleware function that runs for every request
	r.Use(func(c *gin.Context) {
		// Record start time to measure duration
		start := time.Now()

		// c.Next() executes the next handler in the chain
		// Code here runs before the route handler
		c.Next()

		// Code here runs after the route handler returns
		// Calculate duration after the handler finishes
		duration := time.Since(start)
		status := c.Writer.Status()
		// Log the result to stdout
		println("Request took", duration.String(), "with status", status)
	})

	// Define a simple route
	r.GET("/", func(c *gin.Context) {
		// Return JSON response
		c.JSON(http.StatusOK, gin.H{"message": "Hello World"})
	})

	// Start server on port 8080
	r.Run(":8080")
}

Under the hood, Gin maintains a slice of handlers and an index. c.Next() increments the index and calls the function at that position. If the index is past the end, c.Next() does nothing. This implementation makes the chain explicit and predictable.

Order matters. Middleware executes in the order you register it. If you register logging middleware before authentication middleware, the logger runs first. The logger sees the request, passes it to auth, and then runs its post-handling code after auth finishes. If auth rejects the request, the logger still runs its post-handling code. The onion structure ensures that outer layers always wrap inner layers.

Middleware runs in order. The first registered middleware is the outermost layer.

Realistic usage patterns

Real applications need granular control. You do not want authentication on the health check endpoint. Gin lets you attach middleware to groups or specific routes. You can also pass configuration to middleware using closures.

Here's an authentication middleware that checks a token and stores user info in the context.

// AuthMiddleware returns a handler function that checks for a valid token
func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// Extract token from Authorization header
		token := c.GetHeader("Authorization")
		if token == "" {
			// Abort stops the chain and sends a 401 response
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
			// Return is mandatory after abort to prevent c.Next() from running
			return
		}

		// Validate token logic would go here
		// For this example, assume the token is valid
		// Store user info in context for downstream handlers
		c.Set("user_id", "123")
		c.Set("role", "admin")

		// Pass control to the next handler
		c.Next()
	}
}

Convention aside: gin.HandlerFunc is a type alias for func(*gin.Context). Returning a gin.HandlerFunc from a function like AuthMiddleware allows you to capture configuration. This pattern is common when middleware needs a database connection, a secret key, or a list of allowed origins. The outer function takes the config and returns the handler.

Here's how to apply middleware to a group of routes.

func main() {
	r := gin.New()

	// Create a group with middleware
	// Middleware applies to all routes in this group
	admin := r.Group("/admin")
	admin.Use(AuthMiddleware())
	admin.GET("/users", listUsers)
	admin.POST("/users", createUser)

	// Public route without auth
	r.GET("/health", func(c *gin.Context) {
		c.String(http.StatusOK, "OK")
	})

	r.Run(":8080")
}

Downstream handlers can retrieve values set by middleware using c.Get. This method returns the value and a boolean indicating existence. Always check the boolean or use c.MustGet if you are certain the value exists.

func listUsers(c *gin.Context) {
	// Retrieve user_id set by AuthMiddleware
	userID, exists := c.Get("user_id")
	if !exists {
		// This should not happen if middleware is working correctly
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "user context missing"})
		return
	}

	// Use userID to fetch user data
	c.JSON(http.StatusOK, gin.H{"user_id": userID, "users": []string{"alice", "bob"}})
}

Convention aside: gin.Context wraps the standard context.Context. You can access the standard context via c.Request.Context(). Pass this context to database calls or HTTP requests to respect cancellation and deadlines. Functions that take a context should always accept it as the first parameter, conventionally named ctx.

Context is plumbing. Pass it down to every long-lived operation.

Pitfalls and runtime errors

The most common bug is calling c.AbortWithStatusJSON but forgetting to return. The middleware continues to c.Next(), the handler runs, and you get a panic with http: multiple Response.WriteHeader calls. The server tries to write headers twice. Always return immediately after aborting.

Another trap is modifying the request body. Gin buffers the body. If you read the body in middleware, the buffer is consumed. The handler sees an empty body. You must restore the body for the handler to read it. Use c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) to reset the reader.

func BodyLoggingMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// Read body to log it
		body, _ := io.ReadAll(c.Request.Body)
		println("Request body:", string(body))

		// Restore body so downstream handlers can read it
		c.Request.Body = io.NopCloser(bytes.NewBuffer(body))

		c.Next()
	}
}

Convention aside: gofmt is mandatory. Do not argue about indentation or brace placement. Let the tool decide. Most editors run gofmt on save. Consistent formatting reduces cognitive load when reviewing middleware chains.

The compiler rejects code with undefined: io if you forget to import packages. It also complains with imported and not used if you import a package but do not reference it. Middleware often requires io, bytes, and net/http. Keep imports clean.

Middleware is a chain. Break the chain carefully. Abort early, return explicitly.

When to use middleware

Use global middleware via r.Use() when the behavior applies to every request, like logging, CORS headers, or panic recovery.

Use route-specific middleware by passing functions to r.GET() or r.POST() when only certain endpoints need the logic, like rate limiting on expensive routes or request validation on write endpoints.

Use router groups with r.Group().Use() when a set of related routes shares middleware, such as authentication for all admin paths or versioning for API v2 endpoints.

Use gin.HandlerFunc wrappers when your middleware needs configuration, like a database connection, secret key, or allowed origins, passed from the outer function.

Use plain handlers without middleware when the logic is unique to a single route and does not justify the indirection.

Middleware is cheap. Do not over-engineer simple logic.

Where to go next