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:

  1. Implements the IMiddleware interface
  2. Created via constructor functions like new_logger_middleware()
  3. 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.v inside the configure_middleware() function
  • Route-specific middleware goes in routes.v for 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')
}

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!