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.HandlerFuncfunc(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.

Go
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:

Go
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:

Go
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:

Go
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:

Go
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:

Go
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"`
}
Go
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)
})
Bash
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 return after 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 checking c.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 binding tags 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.