Middleware
Middleware is a function that sits between an arriving request and the handler that answers it. It sees the request first, and it decides one of two things: wave it through, or answer it itself and stop there.
That is the whole idea. It earns its place because some questions get asked by a lot of routes — “is this person signed in?” is the same check on fifty pages. Write it once as middleware and each route opts in by listing it.
Chain a few together and you have a series of checkpoints:
request → log it → is it signed in? → is it under the rate limit? → handler → responseAny link can refuse, and if one does, the links after it and your handler never run at all.
You get two layers of this, and they differ in where they sit:
- HTTP middleware wraps the entire router. It runs before Lemmego has worked out which route matches, so it sees every request including ones that will 404.
- Application middleware runs after a route has matched, and receives
Lemmego’s
Contextrather than rawnet/httpvalues.
Reach for the first when something must apply to absolutely everything — recovering from a panic, logging a request. Reach for the second when your check needs to know about your application: who is signed in, whether a form token is valid, whether this person may edit this post.
Don’t worry about setting either of these up to begin with: a project from
lemmego new already installs panic recovery, logging and CSRF for you.
HTTP middleware
HTTP middleware uses the standard library’s own shape,
func(http.Handler) http.Handler — a function that takes the next handler and
returns one wrapping it. That is deliberate: any middleware you already have
for net/http, or find in somebody else’s library, works here unchanged.
These run first, before any routing happens.
Built-in HTTP Middleware
middleware.Recoverer() // Panic recovery
middleware.RequestLogger() // Request logging
middleware.MethodOverride // PUT/DELETE via _method fieldRegistration
HTTP middleware is registered in bootstrap/http_middleware.go:
func LoadHTTPMiddlewares() []app.HTTPMiddleware {
return []app.HTTPMiddleware{
middleware.Recoverer(),
middleware.RequestLogger(),
middleware.MethodOverride,
}
}App Middleware
App middleware uses func(next Handler) Handler and operates on the Lemmego Context.
Built-in App Middleware
middleware.VerifyCSRF(&middleware.CSRFOpts{}) // CSRF protectionRegistration
App middleware is registered in bootstrap/middleware.go:
func LoadMiddlewares() []app.Handler {
return []app.Handler{
middleware.VerifyCSRF(&middleware.CSRFOpts{
ExcludePatterns: []string{"/api/.*"},
}),
}
}Route-Scoped Middleware
Apply middleware to specific routes or groups:
// Before middleware (runs before the handler)
r.UseBefore(auth.Protected)
// Per-route middleware — auth.Protected ships with the framework,
// AdminOnly is one you write yourself (see below)
api.Group("/admin").UseBefore(auth.Protected, middleware.AdminOnly)
// After middleware (runs after the handler)
r.UseAfter(middleware.AuditLog)auth.Protected rejects unauthenticated requests, auth.Guest rejects authenticated
ones (for login/register routes), and auth.OptionalAuth populates the user when one
is present without ever blocking.
Writing Custom Middleware
App Middleware
func RequestTimer(next app.Handler) app.Handler {
return func(c app.Context) error {
start := time.Now()
err := next(c)
duration := time.Since(start)
slog.Info("request completed", "duration", duration)
return err
}
}HTTP Middleware
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
next.ServeHTTP(w, r)
})
}Registration
// bootstrap/middleware.go
func LoadMiddlewares() []app.Handler {
return []app.Handler{
RequestTimer, // Custom app middleware
middleware.VerifyCSRF(nil), // Built-in
}
}
// bootstrap/http_middleware.go
func LoadHTTPMiddlewares() []app.HTTPMiddleware {
return []app.HTTPMiddleware{
CORSMiddleware, // Custom HTTP middleware
middleware.Recoverer(), // Built-in
}
}Middleware Reference
Recovery
Catches panics, logs the stack trace, and returns a 500 error. Supports development mode with stack traces in responses:
middleware.Recovery() // Default options
middleware.RecoveryWithOpts(middleware.DevelopmentRecoveryOpts)
middleware.RecoveryWithCustomFunc(customHandler)CSRF
Protects against cross-site request forgery using session-based tokens. Uses constant-time comparison and supports exclusion patterns:
middleware.VerifyCSRF(&middleware.CSRFOpts{
ExcludePatterns: []string{"/api/.*", "/webhook"},
})The token is a per-session secret: it is stored in the session, mirrored into the XSRF-TOKEN cookie, and not rotated on each request. See CSRF Protection.
Method Override
Enables PUT, PATCH, and DELETE on HTML forms via _method field or X-HTTP-Method-Override header:
middleware.MethodOverrideUsage in forms:
<form method="POST" action="/tasks/1">
<input type="hidden" name="_method" value="DELETE">
</form>