Skip to content

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):

tova
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:

tova
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:

tova
route GET "/protected" with auth => handler

This 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:

tova
route DELETE "/users/:id" with auth, role("admin") => delete_user
route POST "/upload" with auth, validate_body, rate_limit(5) => upload_file

In 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
Response

A middleware can:

  • Modify the request before calling next
  • Short-circuit by returning a response without calling next
  • Modify the response after next returns
  • Measure timing by recording timestamps before and after next

Modifying Requests ​

tova
middleware fn add_timestamp(req, next) {
  req.received_at = Date.now()
  next(req)
}

Short-Circuiting ​

tova
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 ​

tova
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:

tova
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_user

Practical 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.

Released under the MIT License.