Skip to main content

Gin Custom Middleware

Introduction

Middleware is a powerful concept in web development that allows you to execute code before or after an HTTP request is processed. In the Gin framework, middleware provides a way to perform operations like authentication, logging, error handling, and more across multiple routes without duplicating code.

While Gin provides several built-in middleware functions, you'll often need to create custom middleware to implement specific functionality for your application. This guide will teach you how to create and implement your own custom middleware in Gin applications.

What is Custom Middleware?

Custom middleware in Gin is simply a function that:

  1. Takes a gin.Context pointer as a parameter
  2. Performs some operations before and/or after the next middleware or handler is executed
  3. Controls whether the next handlers should be executed using c.Next() or aborts the request using c.Abort()

The basic signature of a Gin middleware function is:

go
func MyCustomMiddleware() gin.HandlerFunc {
// Initialization code (executed once)

return func(c *gin.Context) {
// Code to be executed for each request before the handler is called

c.Next() // Call the next handler

// Code to be executed for each request after the handler is called
}
}

Creating Your First Custom Middleware

Let's create a simple logging middleware that logs the time it took to process a request:

go
package main

import (
"log"
"time"

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

func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Before request
startTime := time.Now()
path := c.Request.URL.Path

// Process request
c.Next()

// After request
endTime := time.Now()
latency := endTime.Sub(startTime)

log.Printf("[%s] %s %s - %d | Latency: %v",
c.Request.Method,
path,
c.Request.URL.RawQuery,
c.Writer.Status(),
latency,
)
}
}

Using Your Custom Middleware

Once you've created a custom middleware function, you can apply it:

  1. Globally - affecting all routes
  2. To specific route groups - affecting only routes in that group
  3. To individual routes - affecting only that specific route

Here's how to use the logger middleware we just created:

go
package main

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

func main() {
r := gin.New() // Create a Gin instance without default middleware

// Apply middleware globally
r.Use(LoggerMiddleware())

// Define routes
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello World!"})
})

// Create a route group with additional middleware
admin := r.Group("/admin")
admin.Use(AuthMiddleware()) // Another custom middleware (defined elsewhere)
{
admin.GET("/dashboard", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Admin Dashboard"})
})
}

r.Run(":8080")
}

Middleware Best Practices

1. Aborting a Request

If your middleware needs to stop the request processing (e.g., for authentication failure), use c.Abort():

go
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Check for auth token
token := c.GetHeader("Authorization")
if token != "valid-token" {
// Stop processing, send error response
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Unauthorized access",
})
// Important: return after aborting
return
}

// If authenticated, proceed to the next middleware/handler
c.Next()
}
}

2. Sharing Data Between Middleware and Handlers

You can use c.Set() and c.Get() to share data between middleware and subsequent handlers:

go
func UserIdentifierMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Extract user ID from token or session
userID := extractUserID(c) // assume this function exists

// Store userID in the context for later handlers
c.Set("userID", userID)

c.Next()
}
}

// Later in a handler:
func getUserProfile(c *gin.Context) {
// Retrieve the userID
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusInternalServerError, gin.H{"error": "User ID not found"})
return
}

// Use the userID
profile := fetchUserProfile(userID.(string))
c.JSON(http.StatusOK, profile)
}

3. Middleware Order Matters

Middleware executes in the order they are added. Consider this sequence carefully:

go
r := gin.New()
r.Use(LoggerMiddleware()) // Executed 1st
r.Use(RecoveryMiddleware()) // Executed 2nd
r.Use(AuthMiddleware()) // Executed 3rd

Practical Examples

Rate Limiting Middleware

This middleware limits the number of requests a client can make in a given time period:

go
func RateLimiterMiddleware(limit int, windowSecs int) gin.HandlerFunc {
// Store of IP addresses and their request counts
// In production, you'd use something like Redis for this
clients := make(map[string]ClientLimit)

type ClientLimit struct {
count int
lastSeen time.Time
}

// Create mutex for thread safety
mu := &sync.Mutex{}

return func(c *gin.Context) {
// Get client IP
ip := c.ClientIP()

mu.Lock()
defer mu.Unlock()

// Check if client exists and reset if time window passed
client, exists := clients[ip]
if !exists || time.Since(client.lastSeen).Seconds() > float64(windowSecs) {
clients[ip] = ClientLimit{count: 1, lastSeen: time.Now()}
c.Next()
return
}

// Increment request count
client.count++
client.lastSeen = time.Now()
clients[ip] = client

// Check if over limit
if client.count > limit {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded. Try again later.",
})
return
}

c.Next()
}
}

Using the rate limiter:

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

// Limit to 5 requests per minute
r.Use(RateLimiterMiddleware(5, 60))

r.GET("/api/data", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Here's your data"})
})

r.Run(":8080")
}

Request Validation Middleware

This middleware validates request parameters before they reach your handlers:

go
func ValidateJSONMiddleware(schema interface{}) gin.HandlerFunc {
return func(c *gin.Context) {
// Create a new instance of the schema
validator := reflect.New(reflect.TypeOf(schema)).Interface()

// Bind JSON to the validator
if err := c.ShouldBindJSON(validator); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Invalid input data",
"details": err.Error(),
})
return
}

// Set the validated data in the context
c.Set("validatedData", validator)
c.Next()
}
}

Using the validation middleware:

go
type UserCreate struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
}

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

r.POST("/users", ValidateJSONMiddleware(UserCreate{}), func(c *gin.Context) {
// Get the validated data
data, _ := c.Get("validatedData")
user := data.(*UserCreate)

// Use the validated data
c.JSON(http.StatusOK, gin.H{
"message": "User created successfully",
"name": user.Name,
"email": user.Email,
})
})

r.Run(":8080")
}

Understanding Middleware Flow

The execution flow of middleware in Gin can be visualized like this:

→ Request comes in
→ Global middleware 1 (pre-processing)
→ Global middleware 2 (pre-processing)
→ Route group middleware (pre-processing)
→ Route specific middleware (pre-processing)
→ Handler function
← Route specific middleware (post-processing)
← Route group middleware (post-processing)
← Global middleware 2 (post-processing)
← Global middleware 1 (post-processing)
← Response goes out

This "onion-layer" structure allows each middleware to perform operations before and after the actual handler is executed.

Summary

Custom middleware in Gin allows you to:

  1. Execute code before and after HTTP requests
  2. Modify request and response objects
  3. End the request-response cycle early
  4. Call the next middleware in the stack

By creating custom middleware, you can extract common functionality that needs to be shared across multiple routes, making your code more maintainable and following the DRY (Don't Repeat Yourself) principle.

Whether you need to implement authentication, logging, rate limiting, or any other cross-cutting concern, custom middleware in Gin provides a clean and efficient way to handle these aspects of your web application.

Exercise Ideas

  1. Caching Middleware: Create a middleware that caches responses for GET requests and serves them from the cache for subsequent identical requests.

  2. Request ID Middleware: Generate a unique ID for each request and add it to the response headers for debugging purposes.

  3. Metrics Middleware: Count and log different types of requests (GET, POST, etc.) and their response statuses.

  4. CORS Middleware: Implement your own custom CORS (Cross-Origin Resource Sharing) middleware from scratch.

Additional Resources

Remember that middleware is a powerful tool in web development, but it's important to keep them focused and efficient to ensure your application performs well.



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)