Middleware & Validation
Gin's middleware model with c.Next(), a custom auth middleware, and request binding/validation with struct tags.
Gin's middleware model
A Gin middleware is any function matching gin.HandlerFunc — func(c *gin.Context). Like net/http middleware, it wraps request handling, but Gin gives you explicit control over the chain via c.Next() and c.Abort() rather than nesting handler functions yourself.
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next() // run the rest of the chain (later middleware + the final handler)
elapsed := time.Since(start)
log.Printf("%s %s -> %d (%s)", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), elapsed)
}
}
Code before c.Next() runs on the way in; code after it runs on the way out, once everything later in the chain (including the actual route handler) has finished — the same "onion" model as ASP.NET Core's middleware pipeline or plain net/http wrapping, just with an explicit Next() call instead of nested function calls.
Register it globally, or scoped to a specific route/group:
r := gin.New()
r.Use(Logger()) // applies to every route
r.GET("/ping", pingHandler) // Logger() runs on every request to this too
A custom auth middleware, with Abort
c.Abort() stops the chain immediately — later middleware and the route handler never run, but code after c.Next() in outer middleware still executes normally on the way back out:
func RequireAPIKey() gin.HandlerFunc {
return func(c *gin.Context) {
key := c.GetHeader("X-API-Key")
if key == "" || key != os.Getenv("API_KEY") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or missing API key"})
c.Abort() // stop here — never reaches the actual handler
return
}
c.Next()
}
}
Apply it only where it's needed, rather than globally:
admin := r.Group("/admin")
admin.Use(RequireAPIKey())
{
admin.DELETE("/users/:id", deleteUserHandler)
}
Request binding
c.ShouldBindJSON decodes the request body into a struct in one call:
type CreateUserInput struct {
Name string `json:"name"`
Email string `json:"email"`
}
r.POST("/users", func(c *gin.Context) {
var input CreateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, input)
})
Validation with binding tags
Gin integrates the go-playground/validator package automatically — add binding struct tags, and ShouldBindJSON validates as part of the same call, before your handler code even runs:
type CreateUserInput struct {
Name string `json:"name" binding:"required,min=2"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=0,lte=130"`
}
r.POST("/users", func(c *gin.Context) {
var input CreateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// input.Name is non-empty and >= 2 chars, input.Email looks like an email,
// input.Age is between 0 and 130 — all guaranteed by this point.
c.JSON(http.StatusCreated, input)
})
curl -X POST http://localhost:8080/users -d '{"name":"A","email":"not-an-email","age":200}'
# 400 {"error":"Key: 'CreateUserInput.Name' Error:Field validation for 'Name' failed on the 'min' tag\n..."}
Common validator tags: required, email, min/max (length for strings, value for numbers), gte/lte, oneof=a b c, dive (validate each element of a slice).
Common mistakes
- Forgetting to
returnafter writing an error response inside a handler or middleware — execution falls through and continues running the rest of the function against invalid/absent data. - Calling
c.Next()and then still writing another response afterward without checkingc.Writer.Written()— attempting to write headers twice logs a warning and is silently ignored for the second write. - Relying only on client-side validation and skipping
bindingtags server-side — the API is reachable directly (via curl, another service, a modified client), so server-side validation is the only validation that actually protects the system.
Interview questions
Q: What's the difference between c.Next() and c.Abort()?
c.Next() continues to the next handler in the chain (the next middleware, or the final route handler) and returns control back to the current middleware afterward. c.Abort() stops the chain immediately — nothing later in the chain runs — but doesn't stop the current function; you should still return right after calling it.
Q: How does Gin validate a request body, and when does that happen?
Via binding struct tags (backed by the go-playground/validator library) checked automatically inside c.ShouldBindJSON (or the other ShouldBind* variants) — decoding and validation happen together in that one call, before your handler's own logic runs, so a handler that reaches its business logic can assume the input already satisfies its tags.
Q: In what order does middleware registered with r.Use() execute relative to route-specific middleware?
Global middleware (registered on the engine or a group with .Use()) runs in registration order, outermost first, before any middleware attached specifically to the matched route, which in turn runs before the final route handler — the same ordering-matters principle as any other middleware pipeline.