Quick Start
Create an App
You need Rust 1.89 or later. Wisp is a pre-release. It installs from code.wispweb.dev because GitHub suspended our account, and moves to crates.io with 1.0. Changelog and Downloads lists every version, the crates and the editor packages, and what is not published yet. wisp new asks a few questions (template, Tailwind, TypeScript only, warnings as errors, build-time type check, git, install). Press Enter for the defaults or add -y. The TypeScript and strictness answers become project rules in Cargo.toml (Config). It also writes rust-toolchain.toml, which pins the Rust version of the Docker image, so every build uses one compiler (Reproducible builds).
cargo install --git https://wisp.ar0.eu/ --tag v1.0.0-pre.3 wisp-web
wisp new guestbook --template minimal
cd guestbook
wisp dev
On Windows, reinstalling with --force fails with "Access is denied" while any wisp.exe runs (wisp dev, or wisp lsp in your editor). Rename it first (ren %USERPROFILE%\.cargo\bin\wisp.exe wisp-old.exe), install, then delete wisp-old.exe once those processes stop.
Open http://127.0.0.1:3000. wisp dev rebuilds as you edit and keeps your browser state. A folder under src/routes is a URL, and the +page.wisp inside it is the page.
Creating a Page
A page is a .wisp file: an optional block of Rust between two --- lines, then markup. This file is served at /hello:
---
let name: String = cx.query_or("name", "world".to_string());
---
<title>Hello page</title>
<meta name="description" content="A page that greets the name given in the query string of the URL.">
<h1>Hello, ${name}!</h1>
The block runs on the server for each request, and ${name} writes an escaped value into the HTML. The <title>, description and <h1> are needed because a new app treats warnings as errors (seo-* in wisp check). Try /hello?name=Ada. A prelude brings Cx, Table, Email, redirect and error into every page, so there are no use lines.
Using a Component
A component is a .wisp file in src/components, and {@props} lists its props. src/components/Note.wisp:
{@props name, message}
<article class="note">
<h2>${name}</h2>
<p>${message}</p>
</article>
<style>
.note {
border-left: 3px solid var(--accent, #7c5cff);
padding-left: 1rem;
}
</style>
Use it by file name, with props as attributes:
<Note name="Ada" message="Hello" />
The <style> block applies to this component only. A missing prop is a build error.
Adding State
A let set to a literal (a number, true/false, a string, a list of those) on its own line in the --- block is browser state. {:count} shows it and on:click changes it. It costs the server nothing per request:
---
let count = 0;
---
<button on:click="count++">Clicked {:count} times</button>
Use a <script> for other browser logic. See Browser code. ${name} is Rust on the server. {:count} is JavaScript in the browser (Server values).
Handling a Form
Example: A Page Is One File shows its code and downloads as a project.
A form posts to an action. The action's parameters are the form's fields, and fields writes a labelled input for each:
---
#[model]
struct Entry {
#[validate(len = 1..=40)]
name: String,
#[validate(len = 1..=200)]
message: String,
}
static ENTRIES: Table<Entry> = Table::saved();
#[action]
fn sign(entry: Entry) {
ENTRIES.add(entry)?;
redirect("/")
}
---
<form action="?/sign" fields>
<button>Sign</button>
</form>
A bad value gives a 422 with each error beside its input and the typed text kept. The server checks every value even though the inputs also get browser checks.
In an action, error() already returns the Result: write return error(404, "Gone");, not Err(error(..)).
Saving Data in a Table
Example: Todos shows its code and downloads as a project.
Table::saved() keeps rows in a log file under WISP_DATA (.wisp/data in dev, data next to a release binary), so they survive a restart. Table::new() keeps them in memory. For a host with no disk, see Where rows are kept.
---
// ...the model, table and action from above...
let entries = ENTRIES.all();
---
<title>Guestbook (${entries.len()})</title>
{#each entries.iter().rev() as entry}
<Note name=${entry.name.as_str()} message=${entry.message.as_str()} />
{:else}
<p>No notes yet. Be the first.</p>
{/each}
When a second page needs the table, move the model and the pub static to src/db.rs (One file or several). Table methods: add, get, all, find, filter, update, set, remove, len (Data).
Check, Test and Build
wisp check # templates, routes, accessibility lints
wisp test # your tests
wisp fmt # format
wisp build # one release binary
wisp build writes one binary. Copy it to a server and run it. It listens on $HOST:$PORT, 0.0.0.0:3000 by default.
Deploying
| You have | Run |
|---|---|
| A VPS or server | wisp build, then wisp service install to keep it running |
| A container host | wisp deploy init docker |
| A static host | wisp build --static (pages with forms need a server) |
| Cloudflare, Deno, Vercel, Netlify or Lambda | wisp build --target <host> |
wisp deploy init <host> writes a workflow or the host's config. An app that signs cookies needs WISP_SECRET (32 or more random characters). See Deploying.
Next Steps
- Tutorial: a guestbook step by step, with tests and a deploy.
- Pages and templates: the Rust block, loads, actions and caching.
- Template syntax and styles: every
{#each},{#if}and snippet. - Actions, forms and UI: validation rules and uploads.
- Browser code: runes, directives, islands and the router.
- APIs and platforms: a JSON API from one struct.
- Deploying and Edge and serverless targets.
- With an AI agent: AI agents and editors covers
AGENTS.mdandwisp mcp.
Is This Page Useful?