Middleware
Middleware functions wrap request handling, letting you run logic before and after route handlers. Common uses include logging, authentication, timing, and request transformation.
Global Middleware
Declare middleware at the server level with the middleware keyword. A middleware function receives the current request and a next function that passes control to the next middleware (or the route handler):
server {
middleware fn logger(req, next) {
start = Date.now()
result = next(req)
duration = Date.now() - start
print("[{req.method}] {req.url} - {duration}ms")
result
}
}Global middleware applies to every route in the server. You can declare multiple global middleware functions, and they execute in the order they are defined:
server {
middleware fn request_id(req, next) {
req.id = generate_id()
next(req)
}
middleware fn logger(req, next) {
start = Date.now()
result = next(req)
print("[{req.id}] {req.method} {req.url} - {Date.now() - start}ms")
result
}
middleware fn error_wrapper(req, next) {
try {
next(req)
} catch err {
print("Error: {err.message}")
respond(500, { error: "Internal server error" })
}
}
}In this example, the execution order is: request_id -> logger -> error_wrapper -> route handler.
Per-Route Middleware
Attach middleware to specific routes using the with keyword:
route GET "/protected" with auth => handlerThis runs the auth middleware before handler. Only this route is affected -- other routes remain unguarded.
Multiple Middleware
Chain multiple middleware functions with commas. They execute left to right:
route DELETE "/users/:id" with auth, role("admin") => delete_user
route POST "/upload" with auth, validate_body, rate_limit(5) => upload_fileIn the first example, auth runs first. If it passes, role("admin") runs next. Only if both succeed does delete_user execute.
Middleware Execution Flow
The middleware chain forms a pipeline around the handler. Each middleware calls next to pass control forward, and can inspect or modify both the request and the response:
Request
|
v
middleware_1(req, next)
| next(req) --> middleware_2(req, next)
| | next(req) --> handler(req)
| | |
| | <-- response <----+
| <-- response <----+
|
v
ResponseA middleware can:
- Modify the request before calling
next - Short-circuit by returning a response without calling
next - Modify the response after
nextreturns - Measure timing by recording timestamps before and after
next
Modifying Requests
middleware fn add_timestamp(req, next) {
req.received_at = Date.now()
next(req)
}Short-Circuiting
fn auth(req, next) {
token = req.headers["authorization"]
if token == nil {
respond(401, { error: "Unauthorized" })
} else {
req.user = verify_jwt(token)
next(req)
}
}When auth returns a 401 response directly, next is never called and the route handler does not run.
Modifying Responses
middleware fn add_cors_headers(req, next) {
response = next(req)
with_headers(response, {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE"
})
}Parameterized Middleware
Middleware that takes configuration returns a middleware function:
fn role(required_role) {
fn(req, next) {
if req.user.role != required_role {
respond(403, { error: "Forbidden" })
} else {
next(req)
}
}
}
// Usage
route DELETE "/users/:id" with auth, role("admin") => delete_userPractical Tips
Order matters for global middleware. Declare logging and request-ID middleware first so they wrap everything. Place error-handling middleware early so it catches errors from all downstream middleware.
Keep middleware focused. Each middleware function should do one thing. Composing small, single-purpose middleware is easier to test and reason about than monolithic functions.
Use short-circuiting judiciously. Returning early from middleware is powerful for auth checks and validation, but be aware that downstream middleware and the handler will not execute.