Forms#

Bialet handles forms with plain HTML and a few Wren methods. A <form> sends data, your .wren file reads it, and you decide what happens next.

The Pattern#

Every form page follows the same three steps:

if (Request.isPost) {
  // 1. Read the fields
  var name = Request.post("name") || ""

  // 2. Process (validate, save)
  `users`.save({"name": name})

  // 3. Redirect to avoid resubmission
  return Response.redirect("/users")
}

// GET — show the form
return <form method="post">
  <input name="name" />
  <button>Save</button>
</form>

⚠️ Pitfall: Request.post(name) returns null when the key is missing. Always guard with || "" or a null check before string operations.

Subscribe Form (POST + Validation)#

A simple email subscription form with basic validation:

// subscribe.wren

var email = Request.post("email") || ""
var error = null

if (Request.isPost) {
  if (!email.contains("@")) {
    error = "Enter a valid email."
  } else {
    `INSERT INTO subscribers (email) VALUES (?)`.query(email)
    return Response.redirect("/subscribe?ok=1")
  }
}

return <main>
  <h1>Subscribe</h1>
  {{ Request.get("ok") && <p class="success">Subscribed!</p> }}
  {{ error && <p class="error">{{ error }}</p> }}
  <form method="post">
    <label>Email: <input name="email" type="email" value="{{ email.safe }}" required /></label>
    <button>Subscribe</button>
  </form>
</main>

Key points:

  • Request.post("email") || "" reads the field safely

  • !email.contains("@") is a quick sanity check — real email validation is complex; this catches most typos and empty submissions in one line

  • var error = null and {{ error && ... }} renders the message inline instead of short-circuiting with return

  • value="{{ email.safe }}" preserves the input on failed submission

  • return Response.redirect(...) prevents resubmission on refresh

  • Request.get("ok") shows a success message after redirect

  • type="email" and required give free browser-side validation

Never trust client-side validation alone. The required attribute is a convenience for the user, not a security boundary.

Search Form (GET)#

For search or filtering, use GET and read query parameters with Request.get():

// search.wren

var q = Request.get("q") || ""
var results = []

if (q != "") {
  results = `SELECT * FROM posts WHERE title LIKE ?`.fetch("%" + q + "%")
}

return <main>
  <h1>Search</h1>
  <form method="get" action="/search">
    <input name="q" value="{{ q.safe }}" placeholder="Search posts..." />
    <button>Search</button>
  </form>

  {{ q != "" && <section>
    <p>{{ results.count }} results for "{{ q.safe }}"</p>
    <ul>
      {{ results.map {|r| <li><a href="/post?id={{ r["id"] }}">{{ r["title"].safe }}</a></li> } }}
    </ul>
  </section> }}
</main>

Key points:

  • method="get" puts the query in the URL (/search?q=term)

  • Request.get("q") || "" avoids null — use || "" so .safe doesn’t crash

  • if (q != "") checks if there’s a search term (empty string is truthy in Wren, so if (q) doesn’t work as a presence check)

  • No redirect needed — GET forms are safe to refresh

  • value="{{ q.safe }}" preserves the search term

  • Escape all user input with .safe when outputting to HTML

Login Form (POST + CSRF + Password)#

A login form with CSRF protection and password verification:

// login.wren

var session = Session.new()
var email = Request.post("email") || ""
var error = null

if (Request.isPost) {
  if (!session.csrfOk) {
    error = "Invalid form submission."
  } else {
    var password = Request.post("password") || ""
    var user = `SELECT * FROM users WHERE email = ?`.first(email)

    if (user && Util.verify(password, user["password"])) {
      session.login(user["id"])
      return Response.redirect("/dashboard")
    }
    error = "Invalid email or password."
  }
}

return <main>
  <h1>Login</h1>
  {{ error && <p class="error">{{ error }}</p> }}
  <form method="post">
    {{ session.csrf }}
    <label>Email: <input name="email" type="email" value="{{ email.safe }}" required /></label>
    <label>Password: <input name="password" type="password" required /></label>
    <button>Login</button>
  </form>
</main>

Key points:

  • Session.csrf renders a hidden token field — put it in every state-changing form

  • Session.csrfOk verifies the token on submit — check it before processing

  • Util.verify(password, hash) checks the password against the stored hash (see Security for how to hash passwords with Util.hash)

  • var error with {{ error && <p class="error">{{ error }}</p> }} shows the message inline on a single form, no duplication

  • value="{{ email.safe }}" preserves the email on failed login

  • session.login(id) persists the session on success

  • return Response.redirect(...) on success, no redirect on failure

⚠️ Pitfall: never store plaintext passwords. Use Util.hash(password) to create the hash, store that in the database, and verify with Util.verify(password, storedHash). See the full Security guide for details on passwords, CSRF, and session defaults.

File Uploads#

Set enctype="multipart/form-data" on the form and read the file with Request.file():

if (Request.isPost) {
  var file = Request.file("attachment")
  if (file) {
    return <p>Uploaded: {{ file.name }} ({{ file.size }} bytes)</p>
  }
}

return <form method="post" enctype="multipart/form-data">
  <input type="file" name="attachment" />
  <button>Upload</button>
</form>

Files are stored in the SQLite database. See the File Handling guide for the full API.

Pitfalls#

  • Request.post() returns null — always || "" before string ops.

  • Missing return before Response.redirect() — causes double-response errors. Always return Response.redirect(...).

  • No .safe on user input in HTML — every untrusted string must be escaped, including in <input value="..."> attributes.

  • Forgetting CSRF on state-changing forms{{ session.csrf }} in the form, session.csrfOk in the handler.

  • Plaintext passwords — use Util.hash / Util.verify, never store raw passwords in the database.

Next Steps#

  • Security — CSRF, password hashing, session defaults, XSS

  • File Handling — uploading, serving, and managing files

  • Database — saving form data with parameterized queries

  • Advanced Routing — where to place form handlers with query params vs _route.wren