Gin Basics #5 Middleware
The previous post covered response handling. This post is about middleware — how to take processing that many handlers need in common, such as logging, recovery, and authentication, and bundle it in one place instead of repeating it in every handler.
- #1 Getting started and your first server
- #2 Routing and handlers
- #3 Request binding and validation
- #4 Responses — JSON, status codes, errors
- #5 Middleware ← this post
- #6 Database integration (GORM)
- #7 Project structure and a mini REST API
What is middleware? #
Middleware is a function that hooks in before and after a handler runs. When a request comes in, it passes through the middleware in order until it reaches the handler, and when the response goes out, it passes back through the middleware in reverse order.
request → middlewareA → middlewareB → handler → middlewareB → middlewareA → responseThis structure lets you separate cross-cutting concerns like “log every request” or “check authentication first for a certain group” from the handler body.
Middleware you were already using — Logger, Recovery #
In #1 we built a router with gin.Default(). At that point two middleware are attached automatically.
- Logger — prints the method, path, status code, and processing time to the console for every request
- Recovery — keeps the server alive when a handler panics and responds with a 500 instead
If you use gin.New() instead, you get an empty engine without these two. Start here when you want to attach exactly the middleware you need.
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())r.Use is the function that registers middleware on the entire engine.
Building custom middleware #
Middleware is the same gin.HandlerFunc type as a handler. In other words, anything shaped like func(c *gin.Context) works. Let’s build a middleware that measures how long a request takes.
func Timer() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next() // run the next middleware and the handler
elapsed := time.Since(start)
log.Printf("%s %s — %v", c.Request.Method, c.Request.URL.Path, elapsed)
}
}The key is c.Next(). Relative to this call, the code above runs before the handler and the code below runs after. After c.Next() runs the next stage, control returns here to log the elapsed time.
Registration is the same as any other middleware.
r := gin.Default()
r.Use(Timer())The reason we wrap middleware in an outer function is to accept configuration values at registration time. For example, you can write Timer(threshold) to take an argument and capture it in a closure.
c.Next() and c.Abort() #
These are the two methods for controlling the flow inside middleware.
c.Next()— proceed to the next middleware and the handlerc.Abort()— stop and skip the remaining stages
Abort does not send a response. So you typically use c.AbortWithStatusJSON, which goes together with a response.
func RequireAPIKey() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("X-API-Key") != "secret" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid key"})
return
}
c.Next()
}
}If the key is wrong, it responds with 401 and ends the middleware function with return. Since Abort is in effect, the handler that follows does not run. If the key is correct, c.Next() lets the request through.
Scope — global, group, route #
A middleware’s scope depends on where you attach it.
r := gin.Default()
// 1) Global — every route
r.Use(Timer())
// 2) Group — only routes inside the group
admin := r.Group("/admin", RequireAPIKey())
{
admin.GET("/stats", showStats)
}
// 3) Route — that one route only
r.GET("/profile", RequireAPIKey(), showProfile)Combined with the router groups we saw in #2, it becomes natural to gather only the endpoints that need authentication into one group and apply the middleware all at once. This is how you split a public API from a protected API by group.
Passing values from middleware to a handler #
Sometimes you want to use a value computed by middleware in the handler that follows. The user information that authentication middleware figures out is a typical example. You store it with c.Set and retrieve it with c.Get.
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
userID := 42 // in reality, extracted from a token
c.Set("userID", userID)
c.Next()
}
}
func profile(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
return
}
c.JSON(http.StatusOK, gin.H{"userID": userID})
}c.Get returns both the value and whether it exists. The type of a stored value is any, so you may need a type assertion when retrieving it. There are also helpers like c.GetInt and c.GetString that retrieve a value with a specified type.
userID := c.GetInt("userID")Full JWT-based authentication will build on top of this pattern in the intermediate series.
Common third-party middleware #
For common processing you don’t have to build yourself, there is official or community middleware.
- CORS —
github.com/gin-contrib/cors - gzip compression —
github.com/gin-contrib/gzip - sessions —
github.com/gin-contrib/sessions
After installing, you register them with r.Use the same way. Operational middleware like CORS and rate limiting will be covered again in the intermediate series.
Wrapping up #
What this post covered:
- Middleware is a
func(c *gin.Context)function that hooks in before and after a handler gin.Default()automatically includes Logger and Recovery- You register with
r.Use, and custom middleware is usually wrapped in a closure to take configuration values c.Next()is the dividing line between before and after the handler runs- The
c.Abortfamily blocks the remaining stages to implement an authentication gate - You can set scope at the global, group, or route level
- Pass values between middleware and a handler with
c.Set/c.Get
In the next post (#6 Database integration) we attach GORM and build a CRUD API that works with real data.