When building APIs with Gin, middleware is one of the most powerful tools in your arsenal. It lets you inject logic into the request-response cycle, enabling you to handle tasks like authentication, logging, rate limiting, and more. While Gin comes with some built-in middleware, the real fun begins when you start writing your own custom middleware. In this post, we’ll explore how to create and use custom middleware in Gin, with plenty of code examples to guide you. And don’t worry—I’ll throw in a few jokes to keep things light. 😉
Why Custom Middleware?
Custom middleware allows you to tailor your API’s behavior to your specific needs. Whether you’re validating API keys, enriching request contexts, or tracking metrics, custom middleware ensures your API stays clean, modular, and maintainable. Plus, it’s surprisingly easy to implement in Gin. Think of it as adding sprinkles to your code—except these sprinkles actually do something useful.
The Anatomy of Gin Middleware
In Gin, middleware is essentially a function that takes a gin.HandlerFunc
and returns a gin.HandlerFunc
. Here’s the basic structure:
func CustomMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Logic before the request reaches the handler
fmt.Println("Before Handler")
// Pass control to the next handler or middleware
c.Next()
// Logic after the handler executes
fmt.Println("After Handler")
}
}
This structure lets you execute code both before and after the main handler. The c.Next()
call is crucial—it tells Gin to proceed to the next middleware or handler in the chain. Without it, your middleware would be like a bouncer who never lets anyone into the club. 🕶️
Example 1: Logging Middleware
Let’s start with a simple logging middleware. This middleware logs the request method, path, and the time it took to process the request.
func LoggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// Log the request method and path
fmt.Printf("Started %s %s\n", c.Request.Method, c.Request.URL.Path)
// Pass control to the next handler
c.Next()
// Log the time taken to process the request
duration := time.Since(start)
fmt.Printf("Completed %s in %v\n", c.Request.URL.Path, duration)
}
}
To use this middleware, attach it to your Gin router:
router := gin.Default()
router.Use(LoggingMiddleware())
router.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
router.Run(":8080")
Now, every time you hit the /ping
endpoint, you’ll see logs like this:
Example 2: Authentication Middleware
Another common use case for middleware is authentication. Let’s create a middleware that checks for a valid API key in the request headers.
Attach this middleware to a specific route:
X-API-Key
header can access the /secure
endpoint.Example 3: Enriching the Request Context
Middleware can also enrich the request context with additional data. For instance, let’s attach a unique request ID to every incoming request.
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := uuid.New().String()
c.Set("requestID", requestID)
// Pass control to the next handler
c.Next()
}
}
You can then retrieve this request ID in your handlers:
This is especially useful for tracing requests across microservices or logging. Think of it as giving each request its own little name tag. 🏷️
Example 4: Error Handling Middleware
Error handling is another area where middleware shines. Instead of repeating error-handling logic in every handler, you can centralize it in a middleware.
func ErrorHandlingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
c.AbortWithStatusJSON(500, gin.H{
"error": "Internal Server Error",
})
}
}()
// Pass control to the next handler
c.Next()
}
}
Attach this middleware globally to catch any panics in your handlers: