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:
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 withc.Abort()
Basic Middleware Chaining
Let's start with a simple example of middleware chaining:
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:
- First middleware's pre-processing code
- Second middleware's pre-processing code
- Route handler code
- Second middleware's post-processing code
- 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:
- All routes using
r.Use()
- Specific routes using the middleware in the route definition
- Groups of routes by applying middleware to a route group
Here's an example demonstrating all three approaches:
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 chainc.Abort()
- Stops the chain, preventing further handlers from executing
Here's an example demonstrating how to use these methods:
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
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
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
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:
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:
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:
- Middleware functions are executed in the order they are added
- Each middleware can execute code before and after the next middleware/handler
- You can control the flow using
c.Next()
andc.Abort()
- Middleware can be applied globally, to groups, or to individual routes
- 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
- Create a middleware that logs all query parameters for each request
- Implement a middleware that blocks requests from specific IP addresses
- Create a middleware that measures response size and logs it
- Build a caching middleware that stores responses for GET requests and serves them from cache when appropriate
- Implement a middleware chain that combines authentication, authorization, and request validation
Additional Resources
- Gin Framework Documentation
- GitHub: Gin Middleware Examples
- Go Web Development Best Practices
- Understanding HTTP Middleware
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)