Wisp

Type to search every docs page and heading.

Wisp

A Fast, Fun Web Framework for Rust

Learn Wisp API Reference

cargo install --git https://wisp.ar0.eu wisp-cli

A Page Is One File

The form writes its own inputs and errors, and a bad value is a 422 that keeps what was typed. The model in src/db.rs is in every route file with no use lines.

---
#[action]
fn add(todo: Todo) {
    TODOS.add(todo);
}
#[action]
fn remove(id: u64) {
    TODOS.remove(id);
}
---
<title>Todos ({TODOS.len()})</title>
<form action="?/add" fields><button>Add</button></form>
{#each TODOS.all() as todo}
  <p>{todo.text} <button action="?/remove&id={todo.id}">Remove</button></p>
{/each}
#[model]
pub struct Todo {
    #[validate(len = 1..=100)]
    text: String,
}
pub static TODOS: Table<Todo> = Table::saved();
localhost:3000

Todos (2)

  • Buy milk
  • Write the docs

Nothing Extra on the Hot Path

A route pays only for the features it uses, and a change that touches the request path is checked by an instructions-per-request A/B before it lands. Wisp's first rule is that speed is never traded away for convenience.

How Speed Is Measured

Measured, Not Claimed

TechEmpower's plaintext and JSON tests, run with their own load scripts against their reference sources of each framework, on one 4-vCPU VM with the server pinned to 2 cores. Medians of 3 runs of 15 seconds each.

Plaintext, 256 connections, pipelined
FrameworkBuilt OnRelative speedReq/s
WispRust1,129,577
FastifyNode.js51,713
ExpressNode.js40,421
SvelteKitSvelte10,929
Next.jsReactOut of memory
JSON, 64 connections
FrameworkBuilt OnRelative speedReq/s
WispRust96,089
FastifyNode.js21,965
ExpressNode.js14,746
SvelteKitSvelte10,378
Next.jsReact1,524

Wisp is first on plaintext at 256, 1,024 and 4,096 connections, and first on JSON at 3 of 6 levels. The tables show the stacks most people know; the full results also include two other Rust servers, Actix Web and Axum, which are close to Wisp on JSON, and Actix Web is ahead at 16 and 256 connections. Next.js ran out of memory on the pipelined plaintext test, so it has no number there. At 16,384 connections Wisp's default limit of 10,000 refuses the overflow, and most servers collapse there too. This is not an official TechEmpower result: the VM is shared, and the gaps between the Rust servers are about the size of its noise.

Full Results

Make the Same App with About Half the Tokens

AI writes most code now, and every token it reads and writes costs time and money. Wisp is built so an app costs the fewest: conventions instead of config, types the compiler infers, and forms that write themselves. The whole reference is one file, llms.txt and AGENTS.md, and wisp mcp serves it to coding agents.

claude mcp add wisp -- wisp mcp
Wisp: a contact form that validates, 89 tokens
+page.wisp
---
fn default(#[validate(len = 1..=50)] name: String, email: Email) {
    eprintln!("{name} <{email}>");
    redirect("/")
}
---
<title>Contact</title>
<form fields><button>Send</button></form>
SvelteKit: the same form, 425 tokens
import { fail, redirect } from '@sveltejs/kit';

export const actions = {
  default: async ({ request }) => {
    const form = Object.fromEntries(await request.formData());
    const errors = {};
    if (!form.name || form.name.length > 50) errors.name = 'must have 1 to 50 characters';
    if (!/^\S+@\S+\.\S+$/.test(form.email)) errors.email = 'must be an email address';
    if (Object.keys(errors).length) return fail(422, { ...form, errors });
    console.log(`${form.name} <${form.email}>`);
    redirect(303, '/');
  }
};
<script>
  import { enhance } from '$app/forms';
  let { form } = $props();
</script>

<svelte:head><title>Contact</title></svelte:head>
<form method="POST" use:enhance>
  <label>Name <input name="name" required minlength="1" maxlength="50" value={form?.name ?? ''} />
    {#if form?.errors?.name}<small class="problem">{form.errors.name}</small>{/if}</label>
  <label>Email <input name="email" type="email" required value={form?.email ?? ''} />
    {#if form?.errors?.email}<small class="problem">{form.errors.email}</small>{/if}</label>
  <button>Send</button>
</form>

The same five features (a list page, a contact form, a JSON endpoint, a layout and a live search) written as a complete app in each stack:

StackRelative sizeTokensFiles
Wisp4646
Nuxt (Vue)8728
SvelteKit9579
Next.js (React)9718
Express (Node.js)1,1567
React (Vite + Express)1,3828

A bigger app, with sign up and in, a posts table, uploads, live refresh and a component, is 995 tokens in Wisp, 3,473 in SvelteKit and 3,331 in Next.js. The numbers come from cargo run -p wisp-tokens, which counts every hand-written file and its path with a byte-pair style estimate; the Tokens page has the method and the apps.

Forms That Work Without JavaScript

A form posts to an action. A bad value is a 422 that shows each problem beside its input and keeps what was typed. With JavaScript on, the page morphs instead of reloading.

---
#[action]
fn signup(email: Email, #[validate(min_len = 8)] password: Password) {
    cx.signup(User { email, password }).await?;
    redirect("/me")
}
---
<form action="?/signup" fields>
  <button>Sign up</button>
</form>

Reactivity in the Same File

The block is Rust that runs for each request, name is drawn on the server, and the count is JavaScript state in the browser. Turn JavaScript off and the server's HTML still works.

---
let name = cx.query_or("name", "world".to_string());
---
<h1>Hello, {name}!</h1>

<button on:click="count++">Clicked {:count} times</button>

<script>
  let count = $state(0)
</script>

One Binary, on Any Host

Templates compile to plain Rust, and the whole app, styles and static files included, becomes one small binary. Every fast path is proven at startup and falls back, and nothing after startup panics. The same app builds as a container, static HTML, or for an edge or serverless host. This site is one too: server-rendered pages that work with JavaScript off, down to the demo and the search form.

wisp build
wisp build --static
wisp build --docker
wisp build --target cloudflare
wisp deploy init cloudflare

Builds for Your Host

A VPS (one binary) Docker Fly.io Render Railway Cloudflare Deno Deploy Vercel Netlify AWS Lambda Bun Node GitHub Pages (static) Cloud Run Azure