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.

Middleware

Middleware runs *before* your route handler. One line per feature — no setup code inside every handler.

On this page

The idea

Middleware runs before your route handler. One line per feature — no setup code inside every handler.

local app = framework()
app:use("cors")   -- done. every response gets CORS headers.
app:use("json")   -- done. every request body is auto-parsed.
app:use("auth")   -- done. unauthenticated requests get 401.

Built-in middleware

Six battle-tested middlewares ship with Wapka:

String What it does
"cors" Adds CORS headers — echoes the request's Origin
"json" Parses JSON request body into ctx.body
"form" Parses URL-encoded form body into ctx.body
"auth" Requires logged-in user — 401 if not
"admin" Requires site admin — 403 if not
"csrf" Validates CSRF token on POST/PUT/DELETE requests

Register them in order. Global middleware first, then your routes:

local app = framework()

-- Global: applied to every route
app:use("cors")
app:use("json")

-- Public routes
app:get("/", homeHandler)
app:get("/blog/:slug", postHandler)

-- Admin-only routes
app:group("/admin", function()
    app:get("/dashboard", adminHome, { middleware = { "auth", "admin" } })
end)

Custom middleware

A middleware is just a function that receives ctx. Use it for logging, timing, custom auth:

-- Log every request
app:use(function(ctx)
    log.info(req.method .. " " .. req.path)
end)

-- Track request timing
app:use(function(ctx)
    ctx._start = os.clock()
end)

Custom middleware can set data on ctx for later handlers:

app:use(function(ctx)
    local token = req.headers["x-api-key"]
    if token then
        ctx.api_client = verifyApiKey(token)
    end
end)

app:get("/api/private", function(ctx)
    if not ctx.api_client then
        return ctx:error("Invalid API key", 401)
    end
    return { data = getPrivateData(ctx.api_client) }
end)

Route-level middleware

Pass middleware in the route options to apply it only to specific routes:

-- Only /api routes need JSON parsing
app:group("/api", function()
    app:get("/users", listUsers, { middleware = "auth" })
    app:post("/users", createUser, { middleware = { "auth", "json" } })
    app:get("/public", publicData)  -- no middleware
end)

CSRF in action

The CSRF middleware checks every POST/PUT/DELETE for a valid token. Exempt webhooks:

app:use("csrf")

-- Protected form (CSRF checked)
app:post("/profile", updateProfile)

-- Webhook (CSRF skipped)
app:post("/webhook/stripe", stripeHandler, { csrf = false })

Middleware execution order

  1. Global middleware (registered first runs first)
  2. Route-level middleware
  3. Route handler
  4. Hooks (before → middleware+handler → after)

Register "auth" and "admin" last so body parsing happens first:

app:use("cors")    -- 1st
app:use("json")    -- 2nd (parse body before checking auth)
app:use("form")    -- 3rd
-- routes here get parsed body + CORS
app:group("/admin", function()
    app:get("/dashboard", handler, { middleware = { "auth", "admin" } })
end)

Next: Hooks — run code before/after every request.

Previous Responses Next Hooks