Skip to main content

Gin Middleware Chaining

Introduction

Middleware is a crucial concept in web development that allows you to process HTTP requests before they reach your route handlers or after the response is generated. In the Gin framework, middleware functions can be chained together to create a pipeline of operations that execute in sequence during request processing.

Middleware chaining is particularly powerful as it enables you to:

  • Apply multiple processing steps to incoming requests
  • Execute code before and after your route handlers
  • Build modular, reusable components for common tasks
  • Control the execution flow by deciding whether to continue the chain or abort it

In this tutorial, we'll explore how middleware chaining works in Gin and how you can leverage this feature to build robust web applications.

Understanding Middleware in Gin

Before diving into chaining, let's quickly review what a Gin middleware function looks like:

go
func SampleMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Code executed before the request is handled

c.Next() // Continue to the next middleware or handler

// Code executed after the request is handled
}
}

A middleware function in Gin:

  • Accepts a gin.Context pointer which contains request and response information
  • Can execute code before calling c.Next()
  • Can execute code after calling c.Next()
  • Can decide whether to continue the chain by calling c.Next() or abort it with c.Abort()

Basic Middleware Chaining

Let's start with a simple example of middleware chaining:

go
package main

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

func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()

// Set a variable in the context
c.Set("example", "12345")

// Before request
fmt.Println("[Logger] Before request")

// Continue to next middleware/handler
c.Next()

// After request
latency := time.Since(t)
fmt.Printf("[Logger] After request, latency: %v\n", latency)
}
}

func Authentication() gin.HandlerFunc {
return func(c *gin.Context) {
// Before request
fmt.Println("[Auth] Authenticating request")

// Continue to next middleware/handler
c.Next()

// After request
fmt.Println("[Auth] Authentication complete")
}
}

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

// Apply middleware to all routes
r.Use(Logger())
r.Use(Authentication())

r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)
fmt.Println("[Handler] Inside the handler with value:", example)
c.JSON(200, gin.H{
"message": "Hello world!",
})
})

r.Run(":8080")
}

When you run this code and make a request to /test, you'll see the following output in your console:

[Logger] Before request
[Auth] Authenticating request
[Handler] Inside the handler with value: 12345
[Auth] Authentication complete
[Logger] After request, latency: 2.123ms

This output demonstrates the order of execution in middleware chaining:

  1. First middleware's pre-processing code
  2. Second middleware's pre-processing code
  3. Route handler code
  4. Second middleware's post-processing code
  5. First middleware's post-processing code

Note how the execution flows like an onion - going through each layer on the way in and then back out in reverse order.

Applying Middleware to Specific Routes

You don't have to apply middleware to all routes. Gin allows you to apply middleware to:

  1. All routes using r.Use()
  2. Specific routes using the middleware in the route definition
  3. Groups of routes by applying middleware to a route group

Here's an example demonstrating all three approaches:

go
package main

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

func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
// Simple auth check
token := c.GetHeader("Authorization")
if token != "valid-token" {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
c.Next()
}
}

func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
// Log the request
c.Next()
}
}

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

// 1. Global middleware - applied to all routes
r.Use(Logger())

// 2. Middleware on a specific route
r.GET("/restricted", AuthRequired(), func(c *gin.Context) {
c.JSON(200, gin.H{"message": "You've accessed a restricted endpoint"})
})

// Public route - no auth required
r.GET("/public", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "This is a public endpoint"})
})

// 3. Group middleware - applied to a group of routes
admin := r.Group("/admin")
admin.Use(AuthRequired())
{
admin.GET("/analytics", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Admin analytics data"})
})

admin.GET("/users", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Admin users data"})
})
}

r.Run(":8080")
}

In this example:

  • The Logger middleware is applied to all routes
  • The AuthRequired middleware is applied to the /restricted route and all routes in the /admin group
  • The /public route doesn't require authentication

Controlling Middleware Flow with Next() and Abort()

One of the most powerful features of middleware chaining is the ability to control the flow of execution:

  • c.Next() - Calls the next handler in the chain
  • c.Abort() - Stops the chain, preventing further handlers from executing

Here's an example demonstrating how to use these methods:

go
package main

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

func RateLimiter() gin.HandlerFunc {
// In a real app, you'd use a proper rate limiting algorithm
return func(c *gin.Context) {
// Example: check if user has exceeded rate limits
limitExceeded := false // This would be a real check

if limitExceeded {
c.AbortWithStatusJSON(429, gin.H{
"error": "Rate limit exceeded",
})
// No further middleware will execute
return
}

fmt.Println("Rate limiter: allowing request")
c.Next() // Continue to next middleware
}
}

func RequestLogger() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Printf("Request received: %s %s\n", c.Request.Method, c.Request.URL.Path)

// Store the status before calling handlers.
// In case of early termination, we'll still have a status code
c.Next()

// Now we have access to the status
statusCode := c.Writer.Status()
fmt.Printf("Request completed with status %d\n", statusCode)
}
}

func main() {
r := gin.New() // Using New() instead of Default() to avoid built-in middleware

r.Use(RequestLogger())
r.Use(RateLimiter())

r.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Success"})
})

r.Run(":8080")
}

When the rate limit hasn't been exceeded, you'll see:

Request received: GET /test
Rate limiter: allowing request
Request completed with status 200

If the rate limit was exceeded, you'd see:

Request received: GET /test
Request completed with status 429

Notice that when c.Abort() is called, the handler never executes, but any middleware that has already started will still complete its post-processing code (after c.Next()).

Real-world Examples

Let's build some practical middleware examples that you might use in real applications:

1. Request Timing Middleware

go
func RequestTimer() gin.HandlerFunc {
return func(c *gin.Context) {
// Start timer
start := time.Now()

// Process request
c.Next()

// Calculate duration
duration := time.Since(start)

// Log the duration
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
if raw != "" {
path = path + "?" + raw
}

log.Printf("%s %s took %v", c.Request.Method, path, duration)

// Add header to the response
c.Header("X-Response-Time", duration.String())
}
}

2. Error Handling Middleware

go
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
// Execute all the handlers
c.Next()

// Look for any errors that might have been set
if len(c.Errors) > 0 {
// You can aggregate and process errors here
c.JSON(500, gin.H{
"errors": c.Errors.Errors(),
})
}
}
}

3. CORS Middleware

go
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, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204) // No content for preflight requests
return
}

c.Next()
}
}

Putting It All Together

Here's how you might combine these middleware in a real application:

go
package main

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

func main() {
r := gin.New() // No default middleware

// Global middleware
r.Use(RequestTimer())
r.Use(ErrorHandler())
r.Use(CorsMiddleware())
r.Use(RequestLogger())

// Public routes
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Welcome"})
})

// Authentication required for API routes
api := r.Group("/api")
api.Use(AuthRequired())
{
api.GET("/users", GetUsers)
api.POST("/users", CreateUser)

// Admin routes with additional authorization
admin := api.Group("/admin")
admin.Use(AdminOnly())
{
admin.GET("/stats", GetStats)
admin.POST("/settings", UpdateSettings)
}
}

r.Run(":8080")
}

Custom Middleware with Parameters

Sometimes you need middleware that can be configured. Here's how to create middleware that accepts parameters:

go
func RateLimiterWithLimit(rps int, burst int) gin.HandlerFunc {
// Create a rate limiter (here using a simple token bucket algorithm)
limiter := rate.NewLimiter(rate.Limit(rps), burst)

return func(c *gin.Context) {
if !limiter.Allow() {
c.AbortWithStatusJSON(429, gin.H{
"error": "Rate limit exceeded",
})
return
}
c.Next()
}
}

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

// Apply rate limiting middleware with different settings per route
r.GET("/public", RateLimiterWithLimit(10, 20), publicHandler)
r.GET("/api", RateLimiterWithLimit(2, 5), apiHandler)

r.Run(":8080")
}

Summary

Middleware chaining in Gin provides a powerful way to process requests through a sequence of operations. Key points to remember:

  1. Middleware functions are executed in the order they are added
  2. Each middleware can execute code before and after the next middleware/handler
  3. You can control the flow using c.Next() and c.Abort()
  4. Middleware can be applied globally, to groups, or to individual routes
  5. The context (c *gin.Context) allows passing data between middleware and handlers

By mastering middleware chaining, you can build modular, maintainable web applications with clean separation of concerns.

Exercises

  1. Create a middleware that logs all query parameters for each request
  2. Implement a middleware that blocks requests from specific IP addresses
  3. Create a middleware that measures response size and logs it
  4. Build a caching middleware that stores responses for GET requests and serves them from cache when appropriate
  5. Implement a middleware chain that combines authentication, authorization, and request validation

Additional Resources

Happy coding!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)