The simple rule
Whatever your handler returns, Wapka figures out the right format:
| You return this | Wapka sends |
|---|---|
A Lua table { a = 1 } |
application/json with {"a":1} |
A string "Hello" |
text/html with Hello |
A number 404 |
That HTTP status code |
That's all you need for 80% of routes. For the other 20%, use the helpers below.
Explicit content types
ctx:json(data)
Force JSON even if your data looks like a string:
app:get("/api/status", function(ctx)
return ctx:json({ ok = true, time = os.date() })
end)
ctx:html(str)
Send raw HTML:
app:get("/terms", function(ctx)
return ctx:html("<h1>Terms of Service</h1><p>...</p>")
end)
ctx:text(str)
Send plain text — no HTML interpretation:
app:get("/robots.txt", function(ctx)
return ctx:text("User-agent: *\nAllow: /")
end)
HTTP status codes
ctx:status(code)
Set the status code. Chainable — you can call .status() then return data:
app:post("/users", function(ctx)
local user = api.users.create(ctx.body)
return ctx:status(201):json(user)
end)
Per-route with a number return
A bare number sets the status:
app:delete("/users/:id", function(ctx)
api.users.delete(tonumber(ctx.params.id))
return 204
end)
ctx:ok(data)
Pass data through unchanged. Useful when a helper already formatted the response:
app:get("/cache", function(ctx)
local cached = getFromCache()
if cached then
return ctx:ok(cached)
end
return { fresh = true }
end)
Error responses
ctx:error(message, code)
Sends a structured error. Always returns { _error = true, message = "...", status = code }:
app:get("/users/:id", function(ctx)
local user = api.users.get(tonumber(ctx.params.id))
if not user then
return ctx:error("User not found", 404)
end
return user
end)
Redirects
ctx:redirect(url, code)
app:post("/logout", function(ctx)
-- clear session...
return ctx:redirect("/")
end)
app:get("/old-page", function(ctx)
return ctx:redirect("/new-page", 301) -- permanent redirect
end)
Default status is 302 (temporary).
Setting response headers
ctx:set_header(name, value)
Chainable. Call before returning:
app:get("/data.csv", function(ctx)
ctx:set_header("Content-Type", "text/csv")
ctx:set_header("Cache-Control", "public, max-age=3600")
return "name,email\nJohn,john@example.com"
end)
Setting cookies
-- Set a cookie that expires in 7 days
ctx:set_cookie("theme", "dark", { max_age = 604800 })
-- Read
local theme = ctx:get_cookie("theme") or "light"
-- Check existence
if ctx:has_cookie("returning_user") then
-- welcome back
end
-- Delete
ctx:delete_cookie("session_temp")
Common response patterns
-- Creation: 201 with the new object
return ctx:status(201):json(user)
-- Deletion: 204 no content
return 204
-- Validation error: 422 with message
return ctx:error("Email is required", 422)
-- Not found: 404
return ctx:error("Post not found", 404)
-- Unauthorized: 401
return ctx:error("Please log in", 401)
Next: Middleware — run code before your handlers.