Middleware
Middleware is the standard shape, so anything written for net/http works unchanged:
type Middleware func(http.Handler) http.Handler
Start from the defaults
ezz.New() installs three, in this order:
| Middleware | What it does |
|---|---|
Recovery(logger) | Catches panics, logs them, replies 500 |
RequestID() | Reuses an incoming X-Request-ID or generates one, and echoes it on the response |
Logging(logger) | Logs method, path, status, duration, remote address, and user agent |
Add more with Use:
import "github.com/sulv-io/ezz/middleware"
app := ezz.New().
Use(
ezz.CORS(ezz.DefaultCORSConfig()),
middleware.Gzip(),
middleware.IPRateLimit(100),
)
Logger(l) rebuilds the default three with the new logger and discards anything added before it. Call Logger first, then Use.Understand the order
The chain is compiled when a route is registered, not per request. Middleware runs outside-in, in the order it was added, and app-level middleware wraps group-level middleware:
auth check
└─ Recovery
└─ RequestID
└─ Logging
└─ your app middleware, in Use order
└─ group middleware, in Use order
└─ handler (binding, validation, your function)
Two consequences are worth knowing before you debug something surprising.
Use after a route is registered has no effect on it. Configure the app fully, then register routes.401 or 403 produced by a route's auth rule is written before Recovery, Logging, or any middleware you added, so those responses are not logged and carry no middleware-added headers. That includes CORS headers, so a browser calling a protected route without a token sees a CORS failure rather than a clean 401. If that matters, handle authentication in middleware you control (see JWT providers) or terminate CORS at your proxy.Add CORS
app.Use(ezz.CORS(ezz.CORSConfig{
AllowedOrigins: []string{"https://app.example.com", "https://admin.example.com"},
AllowedMethods: []string{"GET", "POST", "PATCH", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 3600,
}))
DefaultCORSConfig() allows every origin, the common methods, and Content-Type plus Authorization, with a one-hour preflight cache. Origins are looked up in a map built once at construction, and an OPTIONS request is answered with 200 without reaching your handler. An empty AllowedOrigins means "allow all", so set it explicitly in production.
Compress responses
app.Use(middleware.Gzip())
// or tune it
app.Use(middleware.Gzip(middleware.CompressionConfig{
Level: 6,
MinLength: 2048,
MIMETypes: map[string]bool{"application/json": true},
}))
Defaults are level 6, a 1 KB minimum, and JSON, JavaScript, XML, CSS, HTML, and plain text. Responses are only compressed when the client sends Accept-Encoding: gzip. middleware.DecompressionMiddleware() handles the reverse case, transparently decoding gzipped request bodies.
Rate limit requests
The in-memory limiters are token buckets kept in a map keyed by whatever you choose, with a background sweep for idle keys.
app.Use(middleware.IPRateLimit(100)) // per client IP, per minute
app.Use(middleware.APIKeyRateLimitMiddleware(1000)) // per X-API-Key, else Authorization, else IP
app.Use(middleware.UserRateLimitMiddleware(60, userID)) // per value from your own function
For full control, build a limiter and a key function:
limiter := middleware.NewMemoryLimiter(100, time.Second, 5*time.Minute)
app.Use(middleware.RateLimitMiddleware(limiter, func(r *http.Request) string {
return r.Header.Get("X-Tenant-ID")
}))
middleware.CustomRateLimitMiddleware(capacity, refill, keyFunc) is the same thing in one call. Exceeding the limit returns 429 with a plain-text body. Anything implementing Allow(key string) bool and Reset(key string) can be passed as a limiter, so a shared store is easy to slot in.
auth/ratelimit or your own implementation of the interface.Add middleware to a group
Group middleware only wraps the routes registered on that group, which is the natural place for per-area concerns:
api := app.Group("/api/v1").Use(middleware.Gzip())
internal := app.Group("/internal").Use(requireInternalNetwork())
Write your own
func Timeout(d time.Duration) ezz.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), d)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
app.Use(Timeout(5 * time.Second))
To pass values down to handlers, put them on the request context and read them with ctx.Value:
type tenantKey struct{}
func Tenant(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenant := lookupTenant(r.Host)
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), tenantKey{}, tenant)))
})
}
// in a handler
tenant, _ := ctx.Value(tenantKey{}).(*Tenant)
If your middleware needs to inspect the status or body, wrap the http.ResponseWriter as usual. The writer ezz passes down implements Unwrap() http.ResponseWriter, so http.ResponseController works for flushing and deadlines.
Next steps
- Auth overview for how authentication fits into the same pipeline.
- Performance for what is pooled and precompiled.