Wisp: A Fast, Fun Web Framework for Rust
cargo install --git https://wisp.ar0.eu/ --tag v1.0.0-pre.3 wisp-web
---
#[model]
struct Todo {
#[validate(len = 1..=100)]
text: String,
}
static TODOS: Table<Todo> = Table::saved();
#[action]
fn add(todo: Todo) {
TODOS.add(todo)?;
}
#[action]
fn remove(id: u64) {
TODOS.remove(id)?;
}
---
<title>Todos (${TODOS.len()})</title>
<meta name="description" content="A todo list that saves its rows and checks what you add.">
<h1>Todos</h1>
<form action="?/add" fields />
{#each TODOS.all() as todo}
<p> ${todo.text}
<button action="?/remove&id=${todo.id}">
Remove
</button>
</p>
{/each}
Todos (3)
- Buy milk
- Write the docs
- Learn to code
A Page Is One File
Model, table, actions and markup. The form writes its own inputs and errors.
Pages, forms and validation compile to Rust at build time. A request runs fixed text and a few function calls.
Benchmarks
Same machine, same routes, release builds. The charts show every contender, also where Wisp loses.
The charts use the TechEmpower plaintext and JSON tests. Our script follows the TechEmpower method. This is not an official TechEmpower result. Plaintext uses pipeline depth 16. Each bar is the highest median of at least 5 valid runs over the connection counts.
| Wisp | 4,202,276 |
|---|---|
| Actix Web | 1,773,586 |
| Axum | 1,205,927 |
| Hono on Bun | 229,660 |
| Fastify | 176,034 |
| Express | 121,999 |
| Hono on Node.js | 112,070 |
| Nuxt | 92,976 |
| SvelteKit | 33,042 |
| Next.js | 10,902 |
| Wisp | 460,034 |
|---|---|
| Actix Web | 260,653 |
| Axum | 228,588 |
| Hono on Bun | 213,293 |
| Fastify | 99,949 |
| Nuxt | 85,775 |
| Express | 80,687 |
| Hono on Node.js | 59,993 |
| SvelteKit | 28,945 |
| Next.js | 5,062 |
Dates, machines and the full tables are on the benchmarks page.
Forms Without JavaScript
A bad value sends the form back with the error next to its input and your text kept. With JavaScript on, the page updates in place.
#[model]
struct User {
#[unique]
email: Email,
password: Password,
}
pub static USERS: Table<User> = Table::saved();
---
#[action]
fn signup(email: Email, 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 top block is Rust and runs on the server. The count is JavaScript state in the browser.
---
let name = cx.query_or("name", "world".to_string());
---
<h1>Hello, ${name}!</h1>
<button on:click="count++">Clicked {:count} times</button>
Fewer Tokens
Wisp infers types and uses conventions over config, so code stays short. The table counts the tokens of one small app in each stack.
| Stack | Relative size | Tokens | Files |
|---|---|---|---|
| Wisp | 477 | 7 | |
| Nuxt | 1,043 | 8 | |
| SvelteKit | 1,134 | 9 | |
| Next.js | 1,146 | 8 | |
| Express | 1,297 | 8 | |
| React | 1,599 | 9 | |
| Axum | 1,803 | 7 | |
| Actix Web | 1,878 | 7 |
claude mcp add wisp -- wisp mcp
Editors
The Wisp language server gives highlighting, completion, hover docs, diagnostics and formatting in each editor below.
wisp lsp
One Binary
The app, its styles and static files become one binary. You can also build a container, static HTML, or a target for an edge or serverless host.
wisp build
wisp build --static
wisp deploy init docker
wisp build --target cloudflare
wisp deploy init cloudflare
Testing
Every file is audited within three days of a change. Over a thousand tests run on each change, and the parsers are fuzzed. Wisp is new and no third party has audited it.