ESC
Type to search...
S
Soli Docs

Middleware

Filter HTTP requests and responses. Middleware enables cross-cutting concerns like authentication, logging, and CORS handling.

Request Logging

Soli includes built-in request logging at the server level with timing information:

[LOG] GET /users - 200 (1.234ms)
[LOG] POST /login - 302 (12.876ms)
[LOG] GET /missing - 404 (0.102ms)

To disable request logging, set the environment variable:

# Disable request logging
SOLI_REQUEST_LOG=false soli serve myapp

# Or
SOLI_REQUEST_LOG=0 soli serve myapp

Middleware Attributes

Middleware files in app/middleware/ are loaded automatically. Control behavior using special comment attributes:

Attribute Description
// order: N Execution order (lower runs first, default: 100)
// global_only: true Runs for ALL requests, cannot be scoped to specific routes
// scope_only: true Only runs when explicitly scoped, never globally

Creating Middleware

Middleware functions receive a request hash and return a result hash:

// order: 5
// global_only: true

fn add_cors_headers(req: Any) -> Any {
  // Continue to next middleware/handler
  return {
    "continue": true,
    "request": req
  };
}
// order: 20
// scope_only: true

fn authenticate(req: Any) -> Any {
  headers = req["headers"];
  api_key = "";

  if (has_key(headers, "X-Api-Key")) {
    api_key = headers["X-Api-Key"];
  }

  if (api_key == "") {
    // Short-circuit with error response
    return {
      "continue": false,
      "response": {
        "status": 401,
        "headers": {"Content-Type": "application/json"},
        "body": json_stringify({
          "error": "Unauthorized",
          "message": "API key required"
        })
      }
    };
  }

  // Continue to handler
  return {
    "continue": true,
    "request": req
  };
}

Return Format

Middleware must return a hash with one of these formats:

{
  "continue": true,
  "request": req
}
{
  "continue": false,
  "response": {
    "status": 401,
    "body": "Unauthorized"
  }
}

Execution Order

Each request flows through middleware layers before reaching your controller, then back out as a response.

Incoming
HTTP Request
1 Global
Global Middleware
Runs for every request, sorted by order.
2 Scoped
Route Middleware
Only for routes wrapped in middleware(...).
3 Target
Controller Action
Your handler builds the response.
Outgoing
HTTP Response

Any middleware can short-circuit the chain by returning { "continue": false, "response": ... }, skipping later layers and the controller entirely.

Scoping Middleware to Routes

Use the middleware() function in routes to apply scope-only middleware:

// Public routes (no auth required)
get("/", "home#index");
get("/login", "auth#login");

// Protected routes (auth middleware applied)
middleware("authenticate", fn() {
  get("/dashboard", "dashboard#index");
  get("/profile", "users#profile");
  post("/settings", "users#update_settings");
});

// Multiple middleware
middleware(["authenticate", "admin_only"], fn() {
  get("/admin", "admin#index");
  get("/admin/users", "admin#users");
});

Best Practices

  • • Middleware files in app/middleware/ are loaded automatically — no imports needed
  • • Use order to control execution sequence (lower runs first)
  • • Use global_only: true for middleware that must run on every request (CORS, security headers)
  • • Use scope_only: true for authentication to prevent accidental global application
  • • Keep global middleware lightweight; expensive operations belong in scoped middleware

Next Steps