This episode dissects Echo's middleware system: how to write your own middleware with c.Next, registering it at the root, group, and route levels, pre-middleware with e.Pre, and built-in middleware like RequestLogger, CORS, JWT, BodyLimit, Gzip, and RateLimit.

Middleware is the backbone of an Echo application. Almost every cross-cutting concern — logging, authentication, compression, rate limiting — lives in middleware, not inside handlers. Mastering how to write and register middleware will change how you look at application architecture.
Episode 6 dissects Echo's middleware system: how to write middleware with c.Next(), registering it at the root, group, and route levels, pre-middleware with e.Pre, and the built-in middleware most commonly used in production.
Middleware is a function that wraps a handler and returns a new handler. The key is c.Next() — the point where control passes to the next handler in the chain:
func requestTimer() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()
err := next(c)
slog.Info("durasi", "path", c.Path(), "durasi", time.Since(start))
return err
}
}
}Code before next(c) runs when the request arrives; code after it runs when the response returns. c.Path() returns the route pattern (not the actual URL), which is useful for aggregation.
Middleware can be registered at four levels with different scopes:
e.Use(requestTimer())
admin := e.Group("/admin", middleware.BasicAuth(authFunc))
e.GET("/public", publicHandler, requestTimer())e.Use applies to all routes.group.Use only applies to routes inside the group.e.Pre runs middleware before routing takes place. This is useful for normalization that must happen before the router matches the path:
e.Pre(middleware.RemoveTrailingSlash())
e.Pre(middleware.AddTrailingSlash())The practical difference: e.Use runs after the router picks a route, while e.Pre runs before matching. Use e.Pre for things that modify the request path itself, and e.Use for logic that depends on the selected route.
You already installed both in episode 3: RequestLogger logs every request with slog, and Recover turns panics into 500 responses. These two middleware are the minimum baseline for a production application.
When an API is called from the browser from a different origin, CORS is required:
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://app.example.com"},
AllowMethods: []string{http.MethodGet, http.MethodPost},
AllowHeaders: []string{echo.HeaderContentType},
}))This CORS configuration restricts the origins, methods, and headers that are allowed. In production, don't use AllowOrigins: ["*"] for endpoints that carry credentials.
JWT validates the token on every protected request. Full details are in episode 13, but here's the basic introduction:
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("rahasia-jwt"),
ContextKey: "user",
}))After this middleware, requests without a valid token are rejected before they reach the handler.
Two middleware often installed side by side: BodyLimit limits the body size to prevent abuse, and Gzip compresses responses to save bandwidth:
e.Use(middleware.BodyLimit("1M"))
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Level: 5,
}))The value "1M" means the body is capped at one megabyte. Gzip automatically compresses responses that are large enough.
RateLimit protects the API from bursts of requests. Echo ships an in-memory store that's enough for a single instance:
e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
Store: middleware.NewRateLimiterMemoryStore(20),
}))The configuration above allows 20 requests per second per identity. For multi-instance applications, the store must be replaced with a distributed solution like Redis — covered in episode 19.
Sometimes middleware doesn't need to run for every route. Every built-in middleware accepts a Skipper to exclude specific paths:
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Skipper: func(c echo.Context) bool {
return strings.HasPrefix(c.Path(), "/images/")
},
}))The Skipper function returns true when the middleware should be skipped. This gives you granular control without moving the middleware to the route level.
Episode 6 makes you master Echo's middleware system: writing middleware with c.Next(), registering it at the root, group, and route levels, using e.Pre for the pre-routing stage, and leveraging the built-in CORS, JWT, BodyLimit, Gzip, and RateLimit middleware along with Skipper for flexible control.
Key takeaways:
c.Next() passes control forward.next runs on the way in, after next on the way back.e.Use, group.Use, and route middleware have different scopes.e.Pre runs before routing, for path normalization.BodyLimit limits body size, Gzip compresses responses.RateLimit protects the API from bursts of requests.Skipper excludes middleware for specific routes.In episode 7 next, we'll discuss response, rendering & static files — sending JSON, XML, and HTML, template engines with a custom renderer, response streaming, serving static files, file uploads, and the StaticDirectoryHandler security step related to CVE-2026-55677.