Echo Custom Middleware
Introduction
Middleware functions are an essential part of modern web applications, sitting between the HTTP request and your application's route handlers. In the Echo framework, middleware provides a powerful way to process requests and responses before and after your handler functions execute.
While Echo offers several built-in middleware components, creating custom middleware allows you to extend the framework's capabilities to meet your specific requirements. Custom middleware can help you implement cross-cutting concerns such as logging, authentication, request modification, and performance monitoring in a reusable way.
This guide will walk you through the process of creating custom middleware in Echo, from understanding the basic structure to implementing practical real-world examples.
Understanding Echo Middleware Structure
Before creating custom middleware, it's important to understand the Echo middleware signature:
func(next echo.HandlerFunc) echo.HandlerFunc
This signature means your middleware function should:
- Accept an
echo.HandlerFunc
parameter (the next handler in the chain) - Return an
echo.HandlerFunc
(a function that will be executed)
The returned function should have the signature:
func(c echo.Context) error
This is the actual middleware implementation that receives the Echo context and returns an error.
Creating Basic Custom Middleware
Let's start with a simple example - a middleware that logs the request method and path:
package main
import (
"fmt"
"github.com/labstack/echo/v4"
"net/http"
)
// RequestLoggerMiddleware logs the request method and path
func RequestLoggerMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Code executed before the request is processed
req := c.Request()
fmt.Printf("Request: %s %s\n", req.Method, req.URL.Path)
// Call the next handler
err := next(c)
// Code executed after the request is processed
return err
}
}
func main() {
e := echo.New()
// Apply the middleware to all routes
e.Use(RequestLoggerMiddleware)
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
e.Start(":8080")
}
When you run this code and make a request to /hello
, you'll see the following output in your console:
Request: GET /hello
The middleware executes before the route handler, logs the request information, then passes control to the next middleware or route handler in the chain.
Middleware with Configuration Options
Often, you'll want to configure your middleware. Let's create a more flexible logger middleware with configurable options:
package main
import (
"fmt"
"github.com/labstack/echo/v4"
"net/http"
"time"
)
// LoggerConfig defines the config for Logger middleware
type LoggerConfig struct {
IncludeRequestID bool
IncludeTimestamp bool
IncludeStatusCode bool
}
// LoggerMiddleware returns a middleware that logs HTTP requests
func LoggerMiddleware(config LoggerConfig) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
req := c.Request()
res := c.Response()
startTime := time.Now()
// Build the start of the log message
logMsg := fmt.Sprintf("Request: %s %s", req.Method, req.URL.Path)
// Add optional fields based on configuration
if config.IncludeTimestamp {
logMsg = fmt.Sprintf("[%s] %s", startTime.Format(time.RFC3339), logMsg)
}
if config.IncludeRequestID {
// Generate or get request ID (simplified here)
requestID := c.Request().Header.Get("X-Request-ID")
if requestID == "" {
requestID = fmt.Sprintf("REQ-%d", time.Now().UnixNano())
}
logMsg = fmt.Sprintf("%s [ID:%s]", logMsg, requestID)
}
// Call the next handler
err := next(c)
// Add status code if configured
if config.IncludeStatusCode {
logMsg = fmt.Sprintf("%s [Status:%d]", logMsg, res.Status)
}
// Log the complete message
fmt.Println(logMsg)
return err
}
}
}
func main() {
e := echo.New()
// Configure and apply the middleware
logConfig := LoggerConfig{
IncludeRequestID: true,
IncludeTimestamp: true,
IncludeStatusCode: true,
}
e.Use(LoggerMiddleware(logConfig))
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
e.Start(":8080")
}
With this enhanced middleware, visiting /hello
would produce a log entry like:
[2023-07-03T15:04:05Z] Request: GET /hello [ID:REQ-1625317445000000000] [Status:200]
This demonstrates how to create configurable middleware that can be customized based on your application's needs.
Middleware for Authentication
Authentication is a common use case for middleware. Here's an example of a simple JWT authentication middleware:
package main
import (
"github.com/labstack/echo/v4"
"net/http"
"strings"
"fmt"
)
// JWTAuthMiddleware checks for a valid JWT token in the Authorization header
func JWTAuthMiddleware(secretKey string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get the Authorization header
authHeader := c.Request().Header.Get("Authorization")
// Check if the header exists and has the Bearer prefix
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "missing or invalid authorization header",
})
}
// Extract the token
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
// In a real application, you would validate the token here
// This is a simplified example
if tokenString == "invalid" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "invalid token",
})
}
// For demonstration, we'll just check if the token is not empty
if tokenString == "" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "empty token",
})
}
// For a real application, you would parse and verify the JWT token:
// token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// return []byte(secretKey), nil
// })
// In a real application, you might set user info in the context
// c.Set("user", userInfo)
// Continue with the next handler
return next(c)
}
}
}
func main() {
e := echo.New()
// Public routes
e.GET("/public", func(c echo.Context) error {
return c.String(http.StatusOK, "This is a public endpoint")
})
// Create a group for protected routes
protectedGroup := e.Group("/api")
// Apply JWT middleware to the group
protectedGroup.Use(JWTAuthMiddleware("your-secret-key"))
// Protected routes
protectedGroup.GET("/profile", func(c echo.Context) error {
return c.String(http.StatusOK, "This is a protected endpoint")
})
e.Start(":8080")
}
For this example:
- Accessing
/public
works without authentication - Accessing
/api/profile
requires a valid Bearer token in the Authorization header
Request Modification Middleware
Sometimes you need to modify requests before they reach your handlers. Here's a middleware that adds custom headers to outgoing responses:
package main
import (
"github.com/labstack/echo/v4"
"net/http"
)
// HeadersMiddleware adds custom headers to all responses
func HeadersMiddleware(headers map[string]string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Call the next handler first
err := next(c)
// Add custom headers to the response
for key, value := range headers {
c.Response().Header().Set(key, value)
}
return err
}
}
}
func main() {
e := echo.New()
// Define custom headers
customHeaders := map[string]string{
"X-Framework": "Echo",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
}
// Apply the middleware
e.Use(HeadersMiddleware(customHeaders))
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
e.Start(":8080")
}
With this middleware, all responses will include the custom headers you defined.
Performance Monitoring Middleware
Let's create a middleware that measures the time taken to process each request:
package main
import (
"github.com/labstack/echo/v4"
"net/http"
"time"
"fmt"
)
// PerformanceMonitorMiddleware measures and logs the time taken to process requests
func PerformanceMonitorMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Start timer
startTime := time.Now()
// Process request
err := next(c)
// Calculate processing time
duration := time.Since(startTime)
// Log the performance data
fmt.Printf("[PERF] %s %s - %v\n",
c.Request().Method,
c.Request().URL.Path,
duration)
return err
}
}
func main() {
e := echo.New()
// Apply the middleware
e.Use(PerformanceMonitorMiddleware)
e.GET("/fast", func(c echo.Context) error {
return c.String(http.StatusOK, "This is a fast endpoint")
})
e.GET("/slow", func(c echo.Context) error {
// Simulate a slow operation
time.Sleep(500 * time.Millisecond)
return c.String(http.StatusOK, "This is a slow endpoint")
})
e.Start(":8080")
}
When accessing the /fast
and /slow
endpoints, you would see log entries like:
[PERF] GET /fast - 235.703µs
[PERF] GET /slow - 500.625812ms
This middleware can be extremely useful for identifying performance bottlenecks in your application.
Combining Multiple Custom Middlewares
Echo makes it easy to chain middlewares together. Here's how you can combine several of our custom middlewares:
package main
import (
"github.com/labstack/echo/v4"
"net/http"
)
func main() {
e := echo.New()
// Apply multiple middlewares
e.Use(
// Request logging middleware
RequestLoggerMiddleware,
// Performance monitoring middleware
PerformanceMonitorMiddleware,
// Headers middleware
HeadersMiddleware(map[string]string{
"X-Framework": "Echo",
"Server": "Custom Echo Server",
}),
)
// Group-specific middleware
adminGroup := e.Group("/admin")
adminGroup.Use(JWTAuthMiddleware("admin-secret-key"))
// Routes
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
adminGroup.GET("/dashboard", func(c echo.Context) error {
return c.String(http.StatusOK, "Admin Dashboard")
})
e.Start(":8080")
}
In this example:
- All routes use the RequestLoggerMiddleware, PerformanceMonitorMiddleware, and HeadersMiddleware
- Only routes in the
/admin
group use the JWTAuthMiddleware
Error Handling Middleware
Error handling is another excellent use case for middleware:
package main
import (
"github.com/labstack/echo/v4"
"net/http"
"fmt"
)
// ErrorHandlerMiddleware centralizes error handling
func ErrorHandlerMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Call the next handler
err := next(c)
// If there's an error, handle it
if err != nil {
// Log the error
fmt.Printf("Error: %v\n", err)
// Create a custom error response
if he, ok := err.(*echo.HTTPError); ok {
// If it's an HTTP error, use its status code
return c.JSON(he.Code, map[string]interface{}{
"error": he.Message,
"status": he.Code,
"success": false,
})
}
// For other errors, return a 500 Internal Server Error
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": "Internal Server Error",
"status": http.StatusInternalServerError,
"success": false,
"message": err.Error(), // In production, you might not want to expose this
})
}
// No error occurred
return nil
}
}
func main() {
e := echo.New()
// Apply the error handler middleware
e.Use(ErrorHandlerMiddleware)
// Routes
e.GET("/success", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "This is a successful request",
})
})
e.GET("/error", func(c echo.Context) error {
// Return a standard HTTP error
return echo.NewHTTPError(http.StatusBadRequest, "This is a bad request")
})
e.GET("/panic", func(c echo.Context) error {
// Return a custom error
return fmt.Errorf("something went wrong")
})
e.Start(":8080")
}
With this middleware, errors are captured and transformed into standardized JSON responses instead of Echo's default error handling.
Summary
Custom middleware in Echo provides a powerful way to extend the framework's functionality and handle cross-cutting concerns in your web applications. By creating middleware, you can:
- Log requests and responses
- Authenticate and authorize users
- Modify requests and responses
- Monitor performance
- Handle errors consistently
- Apply business rules across multiple routes
Remember the key points:
- Echo middleware follows a specific signature:
func(next echo.HandlerFunc) echo.HandlerFunc
- Middleware can be applied globally using
e.Use()
, to groups usinggroup.Use()
, or to specific routes - You can create configurable middleware by returning a function that generates the middleware
- Multiple middleware functions are executed in the order they are added
- Middleware can modify both the request (before calling
next()
) and the response (after callingnext()
)
By leveraging custom middleware, you can keep your handlers focused on business logic while moving common functionality into reusable components.
Additional Resources and Exercises
Resources
Exercises
-
Rate Limiting Middleware: Create a middleware that limits the number of requests from a single IP address in a given time window.
-
Request Body Validator: Build a middleware that validates JSON request bodies against a schema before they reach your handlers.
-
Caching Middleware: Implement a middleware that caches responses for GET requests to improve performance.
-
CORS Middleware: Create your own CORS (Cross-Origin Resource Sharing) middleware that handles preflight requests and sets appropriate headers.
-
Logging Middleware with File Output: Extend the logging middleware to write logs to both the console and a rotating log file.
By creating these custom middleware components, you'll gain a deeper understanding of how Echo processes requests and how to extend its functionality to meet your application's needs.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)