Skip to content

28 · HTTP Server

Typed Action Forms

Submit scalar fields to an @action with native HTML and server-side decoding from its signature.

On this page

A progressive form works before JavaScript starts. Bind action to an @action function and name each control after its corresponding parameter. The compiler emits the POST endpoint and a hidden local return path.

Render a form whose action accepts an integer and a checkbox boolean.

33-typed-action-form.zolo
Playground
// Feature: progressive forms take their field types from an @action signature.

// Run with: zolo run examples/features/36-web/33-typed-action-form.zolo --no-cache


use std::html::{View, label, input, button, render}

@action
fn save(age: int, newsletter: bool) -> Result<str, str> {
    if age < 18 {
        return Result::Err("You must be at least 18")
    }
    return Result::Ok("Age: {age}; newsletter: {newsletter}")
}

fn preferences() -> View {
    <form action={save} return_to="/saved" id="preferences">
        <label for="age">Age</label>
        <input id="age" name="age" type="number" required />
        <label for="newsletter">Receive the newsletter</label>
        <input id="newsletter" name="newsletter" type="checkbox" />
        <button type="submit">Save</button>
    </form>
}

print(render(preferences()))

The rendered form uses ordinary HTML. Its id is preserved, age is decoded as int, and an unchecked newsletter becomes false. No parsing code, island, or client script is needed.

Submitted fields Server behavior
age=25&newsletter=on Call save(25, true).
age=25 Call save(25, false).
age=abc, an empty age, or no age HTTP 422 field error; do not call the action.
age=25&age=26 HTTP 422 duplicate-field error; do not call the action.
age=17 Call the action; its business rule returns Result::Err.

The supported parameter types are str, int, float, and bool. A string must be present but may be empty. An integer must fit the exact signed 64-bit range; a float must be finite. Boolean values accept on/true/1 or off/false/0; omission means false. Conversion runs on the server, even when the browser's required or input-type checks are bypassed. Parameter defaults do not fill in omitted fields.

For the full request flow, run this server through the local CLI and open http://localhost:3099. It keeps running until you stop it:

A form page, a typed server action, and a success page.

34-typed-action-form-server.zolo
// Feature: a typed progressive form works through a normal browser POST.

// Run with: zolo run examples/features/36-web/34-typed-action-form-server.zolo --no-cache

// Open http://localhost:3099. No client script or island is needed.


use std::http
use std::html::{View, document, head, title, body, main, h1, p, label, input, button, a}

@action
fn save(age: int, newsletter: bool) -> Result<str, str> {
    if age < 18 {
        return Result::Err("You must be at least 18")
    }
    print("Age: {age}; newsletter: {newsletter}")
    return Result::Ok("Saved")
}

fn home(_req) -> View {
    <document lang="en">
        <head><title>Typed progressive form</title></head>
        <body><main>
            <h1>Preferences</h1>
            <form action={save} return_to="/saved">
                <label for="age">Age</label>
                <input id="age" name="age" type="number" required />
                <label for="newsletter">Receive the newsletter</label>
                <input id="newsletter" name="newsletter" type="checkbox" />
                <button type="submit">Save</button>
            </form>
        </main></body>
    </document>
}

fn saved(_req) -> View {
    <document lang="en">
        <head><title>Saved</title></head>
        <body><main>
            <h1>Saved</h1>
            <p>The action accepted the submitted values.</p>
            <a href="/">Back to preferences</a>
        </main></body>
    </document>
}

let app = http.router()
    |> http.get("/", home)
    |> http.get("/saved", saved)

http.serve(3099, app)

Requires the Zolo CLI/host — open in the playground or run locally.

Submit age 25 with the checkbox unchecked. The handler prints Age: 25; newsletter: false; the browser follows HTTP 303 to /saved. A direct request exercises the same path:

curl -i --data-urlencode "age=25" \
  --data-urlencode "__zolo_return_to=/saved" \
  http://localhost:3099/_zolo/action/features.36-web.34-typed-action-form-server.save

This endpoint belongs to this example file: the path includes the module identity and the action name. In markup, pass only the function reference with action={save}. The compiler writes the correct URL into the rendered form's action attribute; do not reconstruct it from the function name alone.

Decode failures and business Result::Err outcomes both return HTTP 422. This contract does not automatically display errors beside the fields or add pending UI. File uploads, arrays, objects, optional values, variadic parameters, and untyped parameters need a separate endpoint or a later form contract.

return_to defaults to / and accepts only local absolute paths. An explicit method="post" is allowed; other methods are rejected. A URL-valued action="/subscribe" keeps normal HTML behavior. The previous web.action_form(save, return_to: "/saved") { ... } helper remains available with the same typed decoding.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enespt-br