Beta documentation. This is an early preview — content is still in active development. Feedback helps shape the final release. Share your thoughts or join the discussion.

Hooks

Understanding when your code runs helps avoid surprises:

On this page

The request lifecycle

Understanding when your code runs helps avoid surprises:

Request arrives
  → "before" hook fires
    → Global middleware (in registration order)
      → Route matching
        → Route-level middleware
          → Route handler returns
            → "after" hook fires (receives the result)
              → Response sent

  If an error is thrown anywhere:
    → "error" hook fires (receives the error + ctx)

before hook

Runs before route matching. Use it to set up data every handler needs:

app:on("before", function(ctx)
    ctx.request_id = hash.random(8)
    ctx.start_time = os.clock()
end)

app:get("/slow", function(ctx)
    local elapsed = os.clock() - ctx.start_time
    return { elapsed = elapsed, request_id = ctx.request_id }
end)

after hook

Runs after your handler returns. Receives ctx + the handler's return value:

app:on("after", function(ctx, result)
    local elapsed = os.clock() - (ctx.start_time or 0)
    log.info(string.format(
        "%s %s → %dms",
        req.method, req.path, elapsed * 1000
    ))
end)

The result parameter lets you inspect or log the response without modifying it:

app:on("after", function(ctx, result)
    if type(result) == "table" and result._error then
        log.error("Handler error: " .. (result.message or "unknown"))
    end
end)

error hook

Catches unhandled exceptions. Your last line of defense:

app:on("error", function(err, ctx)
    log.error("Unhandled error: " .. tostring(err))
    -- Return a user-friendly response
    return ctx:error("Something went wrong. We've been notified.", 500)
end)

Without this hook, unhandled errors default to a generic 500 response.

Practical patterns

Request timing for every route

app:on("before", function(ctx) ctx._t = os.clock() end)
app:on("after", function(ctx)
    log.info(req.method .. " " .. req.path .. " took " ..
        math.floor((os.clock() - ctx._t) * 1000) .. "ms")
end)

Debug logging only in development

app:on("after", function(ctx, result)
    if log.should_debug() and type(result) == "table" then
        log.debug("Response: " .. encoder.json(result))
    end
end)

Rate limiting per IP

local rate_limit = {}
app:on("before", function(ctx)
    local ip = env.remote_ip
    local now = os.time()
    rate_limit[ip] = rate_limit[ip] or { count = 0, reset = now + 60 }
    if now > rate_limit[ip].reset then
        rate_limit[ip] = { count = 0, reset = now + 60 }
    end
    rate_limit[ip].count = rate_limit[ip].count + 1
    if rate_limit[ip].count > 100 then
        return ctx:error("Too many requests. Try again in a minute.", 429)
    end
end)

Next: Page-Based Code — organize your app across multiple pages.

Previous Middleware Next Page-Based Code