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.

Input Validation

A single unvalidated field can crash your app or corrupt your data. The `validator()` function checks everything before it reaches your logic.

On this page

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.

Previous Page-Based Code Next Static Files (ZIP)