Middleware
Middleware functions intercept HTTP requests before they reach your handlers. Grit ships with four built-in middleware: Auth (JWT validation), CORS, Logger, and Cache. You can also create custom middleware.
Middleware Order & Registration
Middleware is registered in routes/routes.go. The order matters -- middleware executes in the order it is registered.
func Setup(db *gorm.DB, cfg *config.Config, svc *Services) *gin.Engine {r := gin.New()// ── Global middleware (applied to ALL routes) ────────r.Use(middleware.Logger()) // 1. Log every requestr.Use(gin.Recovery()) // 2. Recover from panicsr.Use(middleware.CORS(cfg.CORSOrigins)) // 3. Handle CORS// ── Public routes (no auth required) ────────────────auth := r.Group("/api/auth"){auth.POST("/register", authHandler.Register)auth.POST("/login", authHandler.Login)}// ── Protected routes (auth middleware applied) ──────protected := r.Group("/api")protected.Use(middleware.Auth(db, authService)) // 4. Require JWT{protected.GET("/posts", postHandler.List)}// ── Admin routes (auth + role check) ────────────────admin := r.Group("/api")admin.Use(middleware.Auth(db, authService)) // 4. Require JWTadmin.Use(middleware.RequireRole("admin")) // 5. Require admin role{admin.GET("/users", userHandler.List)admin.DELETE("/users/:id", userHandler.Delete)}return r}
Execution order: Logger runs first, then Recovery, then CORS, then Auth (if on a protected route), then RequireRole (if on an admin route), then finally the handler itself.
Auth Middleware
The Auth middleware extracts the JWT token from the Authorization header, validates it, loads the user from the database, and stores the user in the Gin context for downstream handlers.
func Auth(db *gorm.DB, authService *services.AuthService) gin.HandlerFunc {return func(c *gin.Context) {// 1. Extract the Authorization headerauthHeader := c.GetHeader("Authorization")if authHeader == "" {c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "UNAUTHORIZED","message": "Authorization header is required",},})c.Abort()return}// 2. Parse "Bearer <token>"parts := strings.SplitN(authHeader, " ", 2)if len(parts) != 2 || parts[0] != "Bearer" {c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "UNAUTHORIZED","message": "Invalid authorization header format",},})c.Abort()return}// 3. Validate the JWT tokenclaims, err := authService.ValidateToken(parts[1])if err != nil {c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "UNAUTHORIZED","message": "Invalid or expired token",},})c.Abort()return}// 4. Load user from databasevar user models.Userif err := db.First(&user, claims.UserID).Error; err != nil {c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "UNAUTHORIZED","message": "User not found",},})c.Abort()return}// 5. Check if the account is activeif !user.Active {c.JSON(http.StatusForbidden, gin.H{"error": gin.H{"code": "ACCOUNT_DISABLED","message": "Your account has been disabled",},})c.Abort()return}// 6. Store user data in context for handlersc.Set("user", user)c.Set("user_id", user.ID)c.Set("user_role", user.Role)c.Next()}}
After this middleware runs, handlers can access the authenticated user:
func (h *PostHandler) Create(c *gin.Context) {// Get the full user objectuser, _ := c.Get("user")currentUser := user.(models.User)// Or get individual fieldsuserID, _ := c.Get("user_id") // uintrole, _ := c.Get("user_role") // string}
RequireRole Middleware
RequireRole checks if the authenticated user has one of the specified roles. It must be used after the Auth middleware (which sets user_rolein the context).
// RequireRole checks if the user has one of the required roles.func RequireRole(roles ...string) gin.HandlerFunc {return func(c *gin.Context) {userRole, exists := c.Get("user_role")if !exists {c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "UNAUTHORIZED","message": "Not authenticated",},})c.Abort()return}role, ok := userRole.(string)if !ok {c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "INTERNAL_ERROR","message": "Invalid user role",},})c.Abort()return}for _, r := range roles {if role == r {c.Next()return}}c.JSON(http.StatusForbidden, gin.H{"error": gin.H{"code": "FORBIDDEN","message": "You do not have permission to access this resource",},})c.Abort()}}
Usage examples:
// Admin onlyadmin.Use(middleware.RequireRole("admin"))// Admin or editoreditors.Use(middleware.RequireRole("admin", "editor"))// Any authenticated user (no RequireRole needed, just Auth middleware)
CORS Middleware
The CORS middleware allows your Next.js frontend (running on a different port) to make API requests to the Go backend. It reads the allowed origins from the CORS_ORIGINS environment variable.
func CORS(allowedOrigins []string) gin.HandlerFunc {originsMap := make(map[string]bool)for _, origin := range allowedOrigins {originsMap[origin] = true}return func(c *gin.Context) {origin := c.GetHeader("Origin")if originsMap[origin] {c.Header("Access-Control-Allow-Origin", origin)}c.Header("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, OPTIONS")c.Header("Access-Control-Allow-Headers","Origin, Content-Type, Accept, Authorization")c.Header("Access-Control-Allow-Credentials", "true")c.Header("Access-Control-Max-Age", "86400")if c.Request.Method == http.MethodOptions {c.AbortWithStatus(http.StatusNoContent)return}c.Next()}}
Configure allowed origins in your .env file:
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
Logger Middleware
The Logger middleware logs every HTTP request with its status code, method, path, client IP, and response latency. It runs before all other middleware so it captures the total request time.
func Logger() gin.HandlerFunc {return func(c *gin.Context) {start := time.Now()path := c.Request.URL.Pathquery := c.Request.URL.RawQueryc.Next()latency := time.Since(start)status := c.Writer.Status()method := c.Request.MethodclientIP := c.ClientIP()if query != "" {path = path + "?" + query}log.Printf("[%d] %s %s | %s | %v",status, method, path, clientIP, latency,)}}
Example output:
[200] GET /api/users?page=1 | 127.0.0.1 | 3.241ms[201] POST /api/posts | 127.0.0.1 | 12.507ms[401] GET /api/auth/me | 127.0.0.1 | 0.128ms
Cache Middleware
The CacheResponse middleware caches the full HTTP response for GET requests in Redis. Subsequent identical requests are served from cache with an X-Cache: HIT header.
func CacheResponse(cacheService *cache.Cache, ttl time.Duration,) gin.HandlerFunc {return func(c *gin.Context) {// Only cache GET requestsif cacheService == nil || c.Request.Method != http.MethodGet {c.Next()return}// Build cache key from the full URL (path + query)key := fmt.Sprintf("http:%x",sha256.Sum256([]byte(c.Request.URL.String())),)// Try to serve from cachevar cached cachedResponsefound, err := cacheService.Get(c.Request.Context(), key, &cached)if err == nil && found {c.Header("X-Cache", "HIT")c.Data(cached.Status, cached.ContentType, cached.Body)c.Abort()return}// Capture the responsewriter := &responseCapture{ResponseWriter: c.Writer,body: make([]byte, 0),}c.Writer = writerc.Header("X-Cache", "MISS")c.Next()// Cache successful responsesif writer.status == http.StatusOK && len(writer.body) > 0 {resp := cachedResponse{Status: writer.status,ContentType: writer.Header().Get("Content-Type"),Body: writer.body,}_ = cacheService.Set(c.Request.Context(), key, resp, ttl)}}}
Apply it to specific routes:
// Cache the posts list for 5 minutesprotected.GET("/posts",middleware.CacheResponse(svc.Cache, 5*time.Minute),postHandler.List,)// Cache individual post for 10 minutesprotected.GET("/posts/:id",middleware.CacheResponse(svc.Cache, 10*time.Minute),postHandler.GetByID,)
Creating Custom Middleware
A Gin middleware is any function that returns gin.HandlerFunc. Here is the pattern for creating your own:
package middlewareimport ("net/http""sync""time""github.com/gin-gonic/gin")// RateLimit creates a simple in-memory rate limiter.// maxRequests is the maximum number of requests allowed// within the given window duration per IP address.func RateLimit(maxRequests int, window time.Duration) gin.HandlerFunc {type client struct {count intlastSeen time.Time}var mu sync.Mutexclients := make(map[string]*client)// Clean up old entries periodicallygo func() {for {time.Sleep(window)mu.Lock()for ip, c := range clients {if time.Since(c.lastSeen) > window {delete(clients, ip)}}mu.Unlock()}}()return func(c *gin.Context) {ip := c.ClientIP()mu.Lock()cl, exists := clients[ip]if !exists {clients[ip] = &client{count: 1, lastSeen: time.Now()}mu.Unlock()c.Next()return}if time.Since(cl.lastSeen) > window {cl.count = 1cl.lastSeen = time.Now()mu.Unlock()c.Next()return}cl.count++cl.lastSeen = time.Now()if cl.count > maxRequests {mu.Unlock()c.JSON(http.StatusTooManyRequests, gin.H{"error": gin.H{"code": "RATE_LIMITED","message": "Too many requests, please try again later",},})c.Abort()return}mu.Unlock()c.Next()}}
Register it on routes that need rate limiting:
// Limit auth endpoints to 10 requests per minute per IPauth := r.Group("/api/auth")auth.Use(middleware.RateLimit(10, 1*time.Minute)){auth.POST("/login", authHandler.Login)auth.POST("/register", authHandler.Register)}
Middleware Anatomy
Every Gin middleware follows the same structure:
func MyMiddleware() gin.HandlerFunc {return func(c *gin.Context) {// ── BEFORE the handler ──────────────────────// Check conditions, set context values, etc.// Option A: Continue to next middleware/handlerc.Next()// ── AFTER the handler ───────────────────────// Log results, clean up, etc.// Option B: Stop the chain (use instead of c.Next())// c.Abort()// c.JSON(http.StatusForbidden, gin.H{...})}}
c.Next()passes control to the next middleware or handler in the chain. Code afterc.Next()runs after the handler returns.c.Abort()stops the chain. No further middleware or handlers will execute.c.Set(key, value)stores data in the context for downstream middleware and handlers.c.Get(key)retrieves data stored by upstream middleware.
Built-in Middleware Summary
| Middleware | File | Purpose |
|---|---|---|
| Logger() | middleware/logger.go | Logs every request with status, method, path, latency |
| CORS(origins) | middleware/cors.go | Sets CORS headers, handles preflight OPTIONS |
| Auth(db, svc) | middleware/auth.go | Validates JWT, loads user, sets context |
| RequireRole(roles...) | middleware/auth.go | Checks user role against allowed list |
| CacheResponse(cache, ttl) | middleware/cache.go | Caches GET responses in Redis |