Echo Middleware Chain
Introduction
In Echo, middleware functions are an essential part of the request-response cycle. The middleware chain is a sequence of functions that process HTTP requests and responses. Understanding how this chain works is crucial for building efficient and well-structured web applications in Echo.
A middleware chain allows you to execute a series of operations before or after your route handlers. This concept gives you the power to modularize your application's functionality and apply cross-cutting concerns like logging, authentication, or error handling across multiple routes.
How Middleware Chains Work
In Echo, middleware functions are executed in a sequential order, forming a chain. Each middleware can perform operations before and after the next middleware in the chain:
// Middleware function signature
func(next echo.HandlerFunc) echo.HandlerFunc
When a request comes in, Echo processes it through the middleware chain before reaching the route handler. After the route handler executes, the request flows back through the middleware chain in reverse order.
Let's visualize this process:
Client Request → Middleware 1 → Middleware 2 → Middleware 3 → Route Handler
↓
Client Response ← Middleware 1 ← Middleware 2 ← Middleware 3 ←
Creating a Basic Middleware Chain
Let's create a simple middleware chain to understand the flow:
package main
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// Middleware 1: Logs the request
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
fmt.Println("⬇️ Middleware 1: Request coming in")
err := next(c)
fmt.Println("⬆️ Middleware 1: Response going out")
return err
}
})
// Middleware 2: Add request ID
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Set("request_id", "12345")
fmt.Println("⬇️ Middleware 2: Added request ID")
err := next(c)
fmt.Println("⬆️ Middleware 2: Finishing up")
return err
}
})
// Route handler
e.GET("/hello", func(c echo.Context) error {
fmt.Println("🎯 Route handler: Processing request")
requestID := c.Get("request_id")
return c.String(http.StatusOK, fmt.Sprintf("Hello! Request ID: %v", requestID))
})
e.Start(":8080")
}
When you access the /hello
endpoint, the console output would be:
⬇️ Middleware 1: Request coming in
⬇️ Middleware 2: Added request ID
🎯 Route handler: Processing request
⬆️ Middleware 2: Finishing up
⬆️ Middleware 1: Response going out
This demonstrates the flow of a request through the middleware chain. The request goes through each middleware from top to bottom, then hits the route handler, and finally goes back through each middleware in reverse order.
The next
Function
The next
function plays a crucial role in middleware chains. It represents the next middleware or handler in the chain:
- Calling
next(c)
passes control to the next middleware (or the final handler) - If you don't call
next(c)
, the chain stops, and subsequent middleware or the route handler won't execute - Any code after
next(c)
executes during the response phase
This allows you to perform operations both before and after the rest of the chain executes.
Group-Specific Middleware Chains
You can also apply middleware to specific groups of routes:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// Global middleware
e.Use(middleware.Logger())
// Public routes
e.GET("/public", func(c echo.Context) error {
return c.String(http.StatusOK, "Public content")
})
// Admin group with additional middleware
admin := e.Group("/admin")
admin.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Simple auth check (in real apps, use proper authentication)
authHeader := c.Request().Header.Get("Authorization")
if authHeader != "Admin-Secret" {
return c.String(http.StatusUnauthorized, "Unauthorized")
}
return next(c)
}
})
// Admin routes
admin.GET("/dashboard", func(c echo.Context) error {
return c.String(http.StatusOK, "Admin Dashboard")
})
e.Start(":8080")
}
In this example, all routes have the logger middleware, but only the /admin/*
routes have the authentication middleware.
Practical Example: Building a Complete Middleware Chain
Let's build a more comprehensive example with middleware that:
- Logs request details
- Sets a request ID
- Times the request duration
- Recovers from panics
- Implements CORS
package main
import (
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// Middleware 1: Recover from panics
e.Use(middleware.Recover())
// Middleware 2: Add CORS headers
e.Use(middleware.CORS())
// Middleware 3: Generate request ID
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
requestID := uuid.New().String()
c.Set("request_id", requestID)
c.Response().Header().Set("X-Request-ID", requestID)
return next(c)
}
})
// Middleware 4: Log request details
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()
req := c.Request()
fmt.Printf("⬇️ [%s] %s %s\n",
c.Get("request_id"),
req.Method,
req.URL.Path,
)
err := next(c)
duration := time.Since(start)
fmt.Printf("⬆️ [%s] %s %s - Status: %d - Duration: %v\n",
c.Get("request_id"),
req.Method,
req.URL.Path,
c.Response().Status,
duration,
)
return err
}
})
// Middleware 5: Simple auth for specific routes
authMiddleware := func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token != "valid-token" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Unauthorized access",
})
}
return next(c)
}
}
// Public routes
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"status": "healthy",
"request_id": c.Get("request_id").(string),
})
})
// Protected routes
api := e.Group("/api", authMiddleware)
api.GET("/data", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Protected data accessed successfully",
"request_id": c.Get("request_id"),
"timestamp": time.Now(),
})
})
e.Start(":8080")
}
This example shows how to build a comprehensive middleware chain that handles common web application requirements.
Middleware Chain Order Matters
The order in which you register middleware is important. Middleware registered first will be executed first during the request phase and last during the response phase. Consider this when designing your middleware chain.
Here's a general guideline for ordering middleware:
- Recovery middleware (to catch panics)
- Logger middleware (to log all requests)
- Security middlewares (CORS, CSRF, etc.)
- Request modification middlewares (body parsing, request ID, etc.)
- Route-specific middlewares (authentication, authorization)
Middleware Execution Termination
A middleware can terminate the chain execution by not calling next(c)
:
// Middleware that terminates execution for certain paths
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if c.Path() == "/forbidden" {
return c.String(http.StatusForbidden, "This path is restricted")
}
return next(c)
}
})
If the path is /forbidden
, the middleware returns a response directly without calling next(c)
, so the following middleware and route handlers won't execute.
Skipping Middleware
Sometimes you might want to skip middleware execution for certain routes:
// Skip logging for health check endpoint
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Skipper: func(c echo.Context) bool {
return c.Path() == "/health"
},
}))
Most of Echo's built-in middleware functions support configuration options that include a Skipper
function.
Summary
The Echo middleware chain is a powerful concept that allows you to modularize your application's request-response cycle:
- Middleware functions run in a specific order: from first to last during the request phase, and from last to first during the response phase
- The
next
function determines whether the chain continues or stops - You can apply middleware globally or to specific route groups
- The order of middleware is important for proper application flow
- Middleware can pass information to other middleware and handlers using the Context
Understanding how middleware chains work in Echo helps you build more maintainable, modular, and feature-rich web applications.
Additional Resources
Exercises
- Create a middleware that measures request duration and logs it for requests that take longer than 200ms
- Build a middleware chain that validates an API key and sets rate limiting for API routes
- Implement a middleware that caches responses for specific GET requests
- Create a middleware that tracks the number of concurrent requests and returns a 503 status when a threshold is exceeded
- Build a middleware that adds custom security headers to all responses
These exercises will help you better understand how to implement and combine middleware functions to solve common web application challenges.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)