Middleware Guide
Middleware are functions that execute before (and after) your route handlers. They can modify requests, responses, short-circuit execution, or perform cross-cutting concerns like logging, authentication, and rate limiting.
Table of Contents
What is Middleware?
Middleware wraps route handlers in an "onion" pattern:
Request
↓
Logger (logs request)
↓
Auth (checks authentication)
↓
Rate Limit (checks request quota)
↓
Handler (processes request)
↓
Rate Limit (updates quota)
↓
Auth (no action on response)
↓
Logger (logs response)
↓
Response
Middleware Signature
pub interface IMiddleware {
mut:
handle(mut ctx http.Context, next http.HandlerFunc) http.Response
}
Struct-based middleware:
- Implements the
IMiddlewareinterface - Created via constructor functions like
new_logger_middleware() - Registered with
app.use_middleware(instance)
Built-in Middleware
Varel includes production-ready middleware.
Note: In a real Varel application:
- Global middleware goes in
middleware.vinside theconfigure_middleware()function - Route-specific middleware goes in
routes.vfor individual routes/groups - The examples below show the middleware configuration code only
1. Logger
Logs HTTP requests and responses.
File: middleware.v - Add logger to global middleware:
module main
import leafscale.varel.app as varel_app
import leafscale.varel.middleware
fn configure_middleware(mut app &varel_app.App) {
// Default logger (dev format with colors)
mut logger := middleware.new_logger_middleware(middleware.LoggerConfig{
format: 'dev'
})
app.use_middleware(logger)
// Or custom format
mut logger2 := middleware.new_logger_middleware(middleware.LoggerConfig{
format: 'combined' // dev, common, combined, json
output: .stdout // stdout, stderr, file
colors: true
})
app.use_middleware(logger2)
}
Output formats:
- dev:
GET /users 200 45ms(colored, human-readable) - common: Apache Common Log Format
- combined: Apache Combined Log Format (includes user-agent, referrer)
- json: Structured JSON logs (for production)
2. CORS
Enables Cross-Origin Resource Sharing:
fn main() {
mut web_app := varel_app.new('My App')
// Default CORS (allows all origins)
mut cors := middleware.new_cors_middleware(middleware.CORSConfig{})
web_app.use_middleware(cors)
// Custom CORS configuration
mut cors2 := middleware.new_cors_middleware(middleware.CORSConfig{
allowed_origins: ['https://example.com', 'https://app.example.com']
allowed_methods: ['GET', 'POST', 'PUT', 'DELETE']
allowed_headers: ['Content-Type', 'Authorization']
exposed_headers: ['X-Total-Count']
allow_credentials: true
max_age: 3600 // Preflight cache duration
})
web_app.use_middleware(cors2)
web_app.get('/api/users', api_users)!
web_app.listen(':8080')
}
3. Recovery
Catches panics and returns 500 errors:
fn main() {
mut web_app := varel_app.new('My App')
// Catch panics and return 500
mut recovery := middleware.new_recovery_middleware(middleware.RecoveryConfig{})
web_app.use_middleware(recovery)
web_app.get('/crash', fn (mut ctx http.Context) http.Response {
panic('Something went wrong!')
})!
web_app.listen(':8080')
}
Without recovery, a panic would crash the entire server. With recovery, only that request fails with a 500 error.
4. Rate Limiting
Limits request rate per IP/user:
import time
fn main() {
mut web_app := varel_app.new('My App')
// Default: 100 requests per minute per IP
mut rate_limit := middleware.new_rate_limit_middleware(middleware.RateLimitConfig{
requests_per_window: 100
window_duration: time.minute
})
web_app.use_middleware(rate_limit)
// Strict: 10 requests per minute
mut rate_limit2 := middleware.new_rate_limit_middleware(middleware.RateLimitConfig{
requests_per_window: 10
window_duration: time.minute
})
web_app.use_middleware(rate_limit2)
// Hourly: 1000 requests per hour
mut rate_limit3 := middleware.new_rate_limit_middleware(middleware.RateLimitConfig{
requests_per_window: 1000
window_duration: time.hour
})
web_app.use_middleware(rate_limit3)
// Custom configuration
mut rate_limit4 := middleware.new_rate_limit_middleware(middleware.RateLimitConfig{
requests_per_window: 50
window_duration: time.minute
message: 'Too many requests, slow down!'
})
web_app.use_middleware(rate_limit4)
web_app.get('/api/data', api_data)!
web_app.listen(':8080')
}
Returns 429 Too Many Requests when limit exceeded.
5. CSRF Protection
Protects against Cross-Site Request Forgery:
import os
fn main() {
mut web_app := varel_app.new('My App')
// CSRF protection
// Use config secret or generate with: varel secret generate
secret := os.getenv('CSRF_SECRET') or { config.session.secret_key }
mut csrf := middleware.new_csrf_middleware(middleware.CSRFConfig{
secret: secret
cookie_secure: true // Require HTTPS
})
web_app.use_middleware(csrf)
web_app.get('/form', show_form)!
web_app.post('/submit', submit_form)!
web_app.listen(':8080')
}
fn show_form(mut ctx http.Context) http.Response {
csrf_token := ctx.csrf_token()
html := '<form method="POST" action="/submit">
${middleware.csrf_field_html(csrf_token)}
<input type="text" name="data">
<button type="submit">Submit</button>
</form>'
return ctx.html(200, html)
}
fn submit_form(mut ctx http.Context) http.Response {
// CSRF middleware automatically validates token
// If invalid, returns 403 Forbidden
data := ctx.form('data')
return ctx.ok('Data received: ${data}')
}
6. Compression
Compresses responses with gzip:
fn main() {
mut web_app := varel_app.new('My App')
// Default compression (level 6)
mut compression := middleware.new_compression_middleware(middleware.CompressionConfig{})
web_app.use_middleware(compression)
// Fast compression (level 1)
mut compression2 := middleware.new_compression_middleware(middleware.CompressionConfig{
level: 1
})
web_app.use_middleware(compression2)
// Best compression (level 9)
mut compression3 := middleware.new_compression_middleware(middleware.CompressionConfig{
level: 9
})
web_app.use_middleware(compression3)
web_app.get('/', index)!
web_app.listen(':8080')
}
Automatically compresses responses when:
- Client supports gzip (
Accept-Encoding: gzip) - Response is text-based (HTML, CSS, JS, JSON, XML)
- Response is >= 1KB
7. Session
Manages PostgreSQL-backed user sessions:
import leafscale.varel.vareldb
fn main() {
mut web_app := varel_app.new('My App')
// Connect to PostgreSQL (single connection, lazy initialization)
mut db := vareldb.connect(vareldb.Config{
host: 'localhost'
port: 5432
database: 'myapp_dev'
user: 'postgres'
password: ''
})!
// Configure PostgreSQL-backed sessions
session_config := session.Config{
cookie_name: 'varel_session' // Cookie stores session ID only
max_age: 7 * 24 * 3600 // 7 days
secure: true // HTTPS only (recommended)
http_only: true // Not accessible via JavaScript
same_site: .strict // CSRF protection
db_conn: unsafe { &db } // Database connection (required)
}
mut session_mw := middleware.new_session_middleware(session_config, mut db)!
web_app.use_middleware(session_mw)
web_app.get('/login', show_login)!
web_app.post('/login', do_login)!
web_app.get('/dashboard', dashboard)!
web_app.listen(':8080')
}
fn do_login(mut ctx http.Context) http.Response {
username := ctx.form('username')
password := ctx.form('password')
if authenticate(username, password) {
// Set authenticated user
ctx.set_user(user.id)
return ctx.redirect('/dashboard')
}
return ctx.unauthorized('Invalid credentials')
}
fn dashboard(mut ctx http.Context) http.Response {
// Check if authenticated
if !ctx.is_authenticated() {
return ctx.redirect('/login')
}
// Get authenticated user ID
user_id := ctx.user_id() or {
return ctx.redirect('/login')
}
user := db.get_user(user_id)!
return ctx.html(200, '<h1>Welcome, ${user.name}!</h1>')
}
8. Auth
Requires authentication:
fn main() {
mut web_app := varel_app.new('My App')
// Public routes
web_app.get('/', index)!
web_app.get('/login', show_login)!
web_app.post('/login', do_login)!
// Protected routes
dashboard := web_app.group('/dashboard')
dashboard.use(middleware.auth_required())
dashboard.get('/', dashboard_index)!
dashboard.get('/profile', profile)!
web_app.listen(':8080')
}
Returns 401 Unauthorized if not authenticated.
9. Request ID
Adds unique ID to each request:
fn main() {
mut web_app := varel_app.new('My App')
// Add request ID
mut request_id := middleware.new_request_id_middleware(middleware.RequestIDConfig{})
web_app.use_middleware(request_id)
// Logger will include request ID
mut logger := middleware.new_logger_middleware(middleware.LoggerConfig{
format: 'dev'
})
web_app.use_middleware(logger)
web_app.get('/', fn (mut ctx http.Context) http.Response {
// Access request ID
request_id := ctx.get('request_id')
return ctx.text(200, 'Request ID: ${request_id}')
})!
web_app.listen(':8080')
}
Useful for:
- Log correlation
- Distributed tracing
- Debugging
10. Health Check
Provides health check endpoint:
fn main() {
mut web_app := varel_app.new('My App')
// Add /health endpoint
mut health := middleware.new_health_middleware(middleware.HealthConfig{
path: '/health'
checker: fn () bool {
// Custom health check logic
return db.ping() && cache.ping()
}
})
web_app.use_middleware(health)
web_app.get('/', index)!
web_app.listen(':8080')
}
Returns:
- 200 OK if healthy:
{"status": "ok"} - 503 Service Unavailable if unhealthy:
{"status": "error"}
11. Method Override
Enables HTML forms to send PUT, PATCH, and DELETE requests via POST:
fn main() {
mut web_app := varel_app.new('My App')
// Enable method override (automatically included in new apps)
mut method_override := middleware.new_method_override_middleware(middleware.MethodOverrideConfig{})
web_app.use_middleware(method_override)
// Your routes
web_app.get('/products', list_products)!
web_app.post('/products', create_product)!
web_app.put('/products/:id', update_product)!
web_app.delete('/products/:id', delete_product)!
web_app.listen(':8080')
}
Why it's needed:
HTML forms only support GET and POST methods. To send PUT or DELETE requests, you need JavaScript or method override. Method override is the traditional approach used by Rails, Laravel, and other frameworks.
How it works:
The middleware detects when a POST request includes a _method parameter and overrides the HTTP method before route matching happens.
HTML Form Examples:
<!-- DELETE request via POST + _method -->
<form method="POST" action="/products/123">
<input type="hidden" name="_method" value="DELETE">
<button type="submit">Delete Product</button>
</form>
<!-- PUT request via POST + _method -->
<form method="POST" action="/products/123">
<input type="hidden" name="_method" value="PUT">
<input type="text" name="name" value="Product Name">
<input type="number" name="price" value="99.99">
<button type="submit">Update Product</button>
</form>
<!-- PATCH request via POST + _method -->
<form method="POST" action="/users/456">
<input type="hidden" name="_method" value="PATCH">
<input type="email" name="email" value="user@example.com">
<button type="submit">Update Email</button>
</form>
Via HTTP Header (for APIs):
You can also override the method using the X-HTTP-Method-Override header:
curl -X POST http://localhost:8080/products/123 \
-H "X-HTTP-Method-Override: DELETE"
Security:
- Only POST requests can be overridden (security requirement)
- Only PUT, PATCH, and DELETE are allowed as override methods
- GET requests cannot be changed to DELETE (prevents CSRF attacks)
Custom Configuration:
// Custom override parameter name
mut method_override := middleware.new_method_override_middleware(middleware.MethodOverrideConfig{
param_name: '_http_method' // Default: _method
header_name: 'X-Method-Override' // Default: X-HTTP-Method-Override
allowed_methods: ['PUT', 'DELETE'] // Default: PUT, PATCH, DELETE
})
web_app.use_middleware(method_override)
Scaffold-Generated Forms:
The varel generate scaffold command automatically includes method override in DELETE buttons:
<form method="POST" action="/products/${product.id}" style="display:inline;">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Are you sure?')">Delete</button>
</form>
Implementation Note:
Method override happens at the handler level before route matching, not as traditional middleware. This ensures the router matches the correct route (e.g., DELETE /products/:id) rather than POST /products/:id.
Creating Custom Middleware
Basic Structure
pub fn my_middleware() Middleware {
return fn (next http.HandlerFunc) http.HandlerFunc {
return fn [next] (mut ctx http.Context) http.Response {
// Before handler
println('Before handler')
// Call next middleware/handler
resp := next(mut ctx)
// After handler
println('After handler')
return resp
}
}
}
Example 1: Request Timer
pub fn request_timer() Middleware {
return fn (next http.HandlerFunc) http.HandlerFunc {
return fn [next] (mut ctx http.Context) http.Response {
// Record start time
start := time.now()
// Call next handler
resp := next(mut ctx)
// Calculate duration
duration := time.now() - start
ms := duration.milliseconds()
// Add timing header
resp = resp.header('X-Response-Time', '${ms}ms')
println('Request took ${ms}ms')
return resp
}
}
}
// Usage
fn main() {
mut web_app := varel_app.new('My App')
web_app.use(request_timer())
web_app.get('/', index)!
web_app.listen(':8080')
}
Example 2: API Key Authentication
pub fn api_key_auth(config ApiKeyConfig) Middleware {
return fn [config] (next http.HandlerFunc) http.HandlerFunc {
return fn [next, config] (mut ctx http.Context) http.Response {
// Get API key from header
api_key := ctx.header('X-API-Key')
// Validate
if api_key == '' {
return ctx.unauthorized('API key required')
}
if !config.is_valid(api_key) {
return ctx.forbidden('Invalid API key')
}
// Store in context for handlers
ctx.set('api_key', api_key)
// Continue
return next(mut ctx)
}
}
}
// Usage
fn main() {
mut web_app := varel_app.new('My App')
config := ApiKeyConfig{
valid_keys: ['key1', 'key2', 'key3']
}
api := web_app.group('/api')
api.use(api_key_auth(config))
api.get('/data', api_data)!
web_app.listen(':8080')
}
Example 3: Response Caching
pub fn cache(ttl time.Duration) Middleware {
mut cache_store := map[string]CachedResponse{}
mut lock := sync.new_rwmutex()
return fn [cache_store, lock, ttl] (next http.HandlerFunc) http.HandlerFunc {
return fn [next, cache_store, lock, ttl] (mut ctx http.Context) http.Response {
// Only cache GET requests
if ctx.method() != 'GET' {
return next(mut ctx)
}
cache_key := ctx.path()
// Check cache
lock.@rlock()
cached := cache_store[cache_key] or {
lock.runlock()
goto miss
}
// Check if expired
if time.now() > cached.expires_at {
lock.runlock()
goto miss
}
lock.runlock()
// Cache hit!
return cached.response.header('X-Cache', 'HIT')
miss:
// Cache miss - call handler
resp := next(mut ctx)
// Store in cache
lock.@lock()
cache_store[cache_key] = CachedResponse{
response: resp
expires_at: time.now().add(ttl)
}
lock.unlock()
return resp.header('X-Cache', 'MISS')
}
}
}
struct CachedResponse {
response http.Response
expires_at time.Time
}
// Usage
fn main() {
mut web_app := varel_app.new('My App')
// Cache for 5 minutes
web_app.get('/api/stats', api_stats)!
.use(cache(5 * time.minute))
web_app.listen(':8080')
}
Short-Circuiting
Middleware can stop the chain by not calling next():
pub fn maintenance_mode() Middleware {
return fn (next http.HandlerFunc) http.HandlerFunc {
return fn [next] (mut ctx http.Context) http.Response {
// Check if maintenance mode is enabled
if is_maintenance_mode() {
// Don't call next() - return response directly
return ctx.service_unavailable('Under maintenance')
}
// Not in maintenance - continue
return next(mut ctx)
}
}
}
Middleware Order
Middleware executes in the order it's registered:
fn main() {
mut web_app := varel_app.new('My App')
// Execution order: 1 → 2 → 3 → handler → 3 → 2 → 1
web_app.use(middleware1()) // 1
web_app.use(middleware2()) // 2
web_app.use(middleware3()) // 3
web_app.get('/', handler)!
web_app.listen(':8080')
}
Recommended Order
File: middleware.v - Configure middleware in this order:
import time
fn configure_middleware(mut app &varel_app.App) {
// 1. Recovery - catch panics first
mut recovery := middleware.new_recovery_middleware(middleware.RecoveryConfig{})
app.use_middleware(recovery)
// 2. Request ID - for logging
mut request_id := middleware.new_request_id_middleware(middleware.RequestIDConfig{})
app.use_middleware(request_id)
// 3. Logger - log all requests
mut logger := middleware.new_logger_middleware(middleware.LoggerConfig{
format: 'dev'
})
app.use_middleware(logger)
// 4. Method Override - must be early (handled at handler level)
mut method_override := middleware.new_method_override_middleware(middleware.MethodOverrideConfig{})
app.use_middleware(method_override)
// 5. CORS - handle OPTIONS requests early
mut cors := middleware.new_cors_middleware(middleware.CORSConfig{})
app.use_middleware(cors)
// 6. Compression - compress responses
mut compression := middleware.new_compression_middleware(middleware.CompressionConfig{})
app.use_middleware(compression)
// 7. Rate limiting - prevent abuse
mut rate_limit := middleware.new_rate_limit_middleware(middleware.RateLimitConfig{
requests_per_window: 100
window_duration: time.minute
})
app.use_middleware(rate_limit)
// 8. Session - load sessions (if using sessions)
// mut session_mw := middleware.new_session_middleware(session_config)
// app.use_middleware(session_mw)
// 9. CSRF - validate tokens (if needed)
// mut csrf := middleware.new_csrf_middleware(csrf_config)
// app.use_middleware(csrf)
}
Common Patterns
1. Conditional Middleware
Run middleware only for certain conditions:
pub fn conditional(condition fn (http.Context) bool, mw Middleware) Middleware {
return fn [condition, mw] (next http.HandlerFunc) http.HandlerFunc {
return fn [next, condition, mw] (mut ctx http.Context) http.Response {
if condition(ctx) {
// Apply middleware
return mw(next)(mut ctx)
}
// Skip middleware
return next(mut ctx)
}
}
}
// Usage
fn main() {
mut web_app := varel_app.new('My App')
// Only log API requests
web_app.use(conditional(
fn (ctx http.Context) bool {
return ctx.path().starts_with('/api/')
},
middleware.logger_default()
))
web_app.listen(':8080')
}
2. Middleware Chain
Combine multiple middleware:
pub fn api_middleware_chain() []Middleware {
return [
middleware.rate_limit_strict(),
middleware.api_key_auth(),
middleware.json_only(),
]
}
// Usage
fn main() {
mut web_app := varel_app.new('My App')
api := web_app.group('/api')
for mw in api_middleware_chain() {
api.use(mw)
}
api.get('/data', api_data)!
web_app.listen(':8080')
}
3. Context Values
Share data between middleware and handlers:
pub fn user_loader() Middleware {
return fn (next http.HandlerFunc) http.HandlerFunc {
return fn [next] (mut ctx http.Context) http.Response {
// Get user ID from session
user_id := ctx.session_get('user_id') or {
return next(mut ctx)
}
// Load user
user := db.get_user(user_id.int()) or {
return next(mut ctx)
}
// Store in context
ctx.set('current_user', user)
return next(mut ctx)
}
}
}
// Access in handler
fn dashboard(mut ctx http.Context) http.Response {
user := ctx.get('current_user') // User loaded by middleware
return ctx.html(200, '<h1>Welcome, ${user.name}!</h1>')
}
Summary
You've learned:
✅ What middleware is and how it works ✅ All built-in middleware and their configurations ✅ How to create custom middleware ✅ Middleware execution order ✅ Common patterns (conditional, chains, context)
Continue to the Controllers & MVC Guide to learn about organizing your application!