How to Add Logging, CORS, and Authentication Middleware in Echo for Golang Clean Architecture
To add logging, CORS, and authentication middleware in Echo within a Clean Architecture project, register middleware.Logger(), middleware.CORS(), and middleware.JWT() in the infrastructure layer's router setup, specifically in pkg/infrastructure/router/router.go, ensuring all framework-specific concerns remain isolated from business logic.
The manakuro/golang-clean-architecture repository demonstrates how to structure a Go application using Clean Architecture principles while leveraging the Echo web framework. When adding middleware for logging, CORS, and authentication in Echo, it is critical to keep these infrastructure concerns confined to the router layer, ensuring that controllers and use cases remain framework-agnostic and testable.
Understanding the Clean Architecture Router Setup
In this repository, the Echo router is initialized in pkg/infrastructure/router/router.go. The NewRouter function already wires two essential middleware components that provide basic observability:
e.Use(middleware.Logger()) // request logging
e.Use(middleware.Recover()) // panic recovery
These built-in Echo middleware functions handle request logging and panic recovery. Because this file resides in the infrastructure layer, it is the appropriate location to add additional cross-cutting concerns like CORS and authentication without violating the dependency rule of Clean Architecture or leaking framework details into the domain layer.
Adding CORS Middleware in Echo
Cross-Origin Resource Sharing (CORS) is essential for browser-based clients. Echo provides the middleware.CORS() function, which can be applied globally in NewRouter immediately after the logger and recovery middleware.
// ---------- CORS ----------
// Allow any origin – replace with a stricter config in production.
e.Use(middleware.CORS())
For production environments requiring stricter security, use middleware.CORSWithConfig() with a middleware.CORSConfig struct to specify allowed origins, methods, and headers explicitly:
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://example.com"},
AllowMethods: []string{echo.GET, echo.POST},
AllowHeaders: []string{echo.HeaderContentType, echo.HeaderAuthorization},
}))
Implementing JWT Authentication Middleware
Authentication is implemented using Echo's middleware.JWT(). The signing secret is stored in config/config.yml and loaded via Viper in pkg/config/config.go, ensuring secrets are not hard-coded.
First, add the JWT secret to your configuration file:
# File: config/config.yml
jwt:
secret: "YOUR_SUPER_SECRET_KEY"
server:
address: ":8080"
database:
user: "root"
password: "password"
net: "tcp"
addr: "127.0.0.1:3306"
dbname: "mydb"
allowNativePasswords: true
params:
parseTime: "True"
The ReadConfig function unmarshals this file into the global config.C variable. Then, initialize the JWT middleware in NewRouter using the secret from config.C.JWT.Secret:
// ---------- JWT Authentication ----------
jwtSecret := []byte(config.C.JWT.Secret)
authGroup := e.Group("", middleware.JWT(jwtSecret))
The middleware.JWT function validates the Authorization: Bearer <token> header and populates the Echo context with claims accessible via ctx.Get("user").
Protecting Routes with Authentication
In Clean Architecture, controllers should not handle authentication logic directly. Instead, use Echo's route groups to apply middleware selectively, keeping public and protected endpoints clearly separated.
Public routes remain on the root router:
// Public endpoints
e.GET("/users", func(ctx echo.Context) error { return c.User.GetUsers(ctx) })
e.POST("/users", func(ctx echo.Context) error { return c.User.CreateUser(ctx) })
Protected routes use the authGroup created earlier:
// Protected route example
authGroup.GET("/profile", func(ctx echo.Context) error {
// Retrieve JWT claims injected by middleware
claims := ctx.Get("user").(*jwt.Token).Claims.(jwt.MapClaims)
userID := claims["sub"].(string)
// Pass to controller method
return c.User.GetProfile(ctx, userID)
})
Controllers receive the Echo context via the Context interface defined in pkg/adapter/controller/context.go, allowing them to retrieve user identity when necessary while remaining decoupled from the JWT implementation details.
Summary
- Infrastructure isolation: All Echo middleware—including logging, CORS, and JWT authentication—belongs in
pkg/infrastructure/router/router.goto maintain Clean Architecture boundaries and prevent framework leakage into business logic. - Built-in observability: The repository already implements
middleware.Logger()andmiddleware.Recover()for request logging and panic recovery. - CORS configuration: Add
middleware.CORS()globally to handle cross-origin requests, or usemiddleware.CORSWithConfig()for production-specific origin restrictions. - JWT authentication: Store the signing secret in
config/config.yml, load it viaconfig.C.JWT.Secret, and applymiddleware.JWT()to route groups to protect specific endpoints while keeping public routes accessible. - Context propagation: Controllers retrieve JWT claims via
ctx.Get("user")without importing authentication logic into use-case layers, preserving testability and architectural integrity.
Frequently Asked Questions
Where should middleware be registered in a Clean Architecture Go project?
Middleware should be registered in the infrastructure layer, specifically within the router setup file at pkg/infrastructure/router/router.go. This ensures that framework-specific concerns like logging, CORS, and JWT validation remain isolated from domain and use-case layers, preserving the dependency rule that inner circles should not depend on external frameworks.
How do I configure CORS for specific origins instead of allowing all?
Instead of using the permissive middleware.CORS(), use middleware.CORSWithConfig() with a middleware.CORSConfig struct. Specify the AllowOrigins slice with your trusted domains, and optionally restrict AllowMethods and AllowHeaders. This configuration should be added in NewRouter after the logger and recovery middlewares but before route definitions.
Can I apply JWT authentication to only specific routes while keeping others public?
Yes. Rather than applying middleware.JWT() globally with e.Use(), create a route group using e.Group("", middleware.JWT(jwtSecret)). This group acts as a middleware pipeline that only applies to routes registered on that group. Public routes can remain on the root echo.Echo instance, allowing you to mix protected and unprotected endpoints while maintaining Clean Architecture separation.
How does the controller access the authenticated user identity without breaking Clean Architecture?
The Echo JWT middleware stores parsed token claims in the context under the key "user". Inside a controller method, retrieve the claims using ctx.Get("user").(*jwt.Token).Claims.(jwt.MapClaims), then extract specific fields like the subject (sub) or user ID. Because the controller receives an abstraction of the context (via the Context interface in pkg/adapter/controller/context.go), and the JWT logic remains in the infrastructure layer, the use-case layer remains completely decoupled from authentication concerns.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →