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.