Never trust user input
A single unvalidated field can crash your app or corrupt your data. The validator() function checks everything before it reaches your logic.
local v = validator(data, rules)
if v:passes() then
local clean = v:valid() -- safe to use
else
local first_error = v:first() -- show user
local all_errors = v:errors() -- log or debug
end
Basic usage
app:post("/register", function(ctx)
local v = validator(req.post, {
username = "required|string|min:3|max:30",
email = "required|email",
password = "required|string|min:8",
age = "required|number|min:13"
})
if v:fails() then
ctx:flash("error", v:first()) -- "Email is not valid"
ctx:flash("errors", v:errors()) -- all field errors
return ctx:redirect("/register")
end
-- v:valid() returns ONLY the fields in your rules — nothing extra
local user = api.users.create(v:valid())
return ctx:redirect("/welcome")
end)
All validation rules
Rules are pipe-separated strings. Order doesn't matter.
| Rule | What it checks | Example |
|---|---|---|
required |
Field must be present and non-empty | "required" |
string |
Must be a string | "required\|string" |
number |
Must be a number (or numeric string) | "number" |
email |
Valid email format | "required\|email" |
boolean |
true, false, 0, or 1 |
"boolean" |
table |
Must be a Lua table | "table" |
min:N |
Minimum value (numbers) or length (strings) | "min:3" |
max:N |
Maximum value or length | "max:100" |
in:a,b,c |
Value must be one of these | "in:admin,user,editor" |
regex:pattern |
Must match a Lua pattern | "regex:^[%w_]+$" |
nullable |
Skip validation if field is empty or missing | "nullable\|email" |
Combining rules
-- Username: required, not too short, not too long
"required|string|min:3|max:30"
-- Bio: optional, but if provided, max 500 chars
"nullable|string|max:500"
-- Role: must be one of three values
"required|in:admin,editor,viewer"
-- Custom field: must match pattern
"required|regex:^[A-Z][a-z]+$"
The validator methods
v:passes() → boolean -- all rules passed
v:fails() → boolean -- opposite of passes()
v:errors() → table -- { field = "error message", ... }
v:first() → string -- first error message (great for flash)
v:valid() → table -- only validated fields, clean and safe
Practical patterns
API endpoint with JSON body
app:use("json")
app:post("/api/products", function(ctx)
local v = validator(ctx.body, {
name = "required|string|min:2|max:200",
price = "required|number|min:0",
stock = "required|number|min:0",
tags = "nullable|string"
})
if v:fails() then
return ctx:error(v:errors(), 422)
end
local product = api.dataset.create("products", v:valid())
return ctx:status(201):json(product)
end)
Optional fields
-- Phone is optional, but if provided, must match format
local v = validator(req.post, {
name = "required|string",
phone = "nullable|regex:^%+?[%d%-%(%)%s]+$",
bio = "nullable|string|max:500"
})
Building a reusable validation helper
function validateOrRedirect(data, rules, redirect_url)
local v = validator(data, rules)
if v:fails() then
ctx:flash("error", v:first())
ctx:flash("errors", v:errors())
ctx:flash("old", data)
return nil, ctx:redirect(redirect_url)
end
return v:valid()
end
-- Usage
local data = validateOrRedirect(req.post, {
title = "required|string|min:3"
}, "/edit")
if not data then return end -- already redirected
Next: Static Files — serve ZIP bundles as static assets.