Part 14 of the Fitz series. Part 13 showed the dev loop for
.fitzv→ WASM; this part is what a.fitzvcomponent actually is, and how a language that emits native binaries also emits a frontend — to WebAssembly, without adopting a JavaScript framework.
The problem: a server language needs a frontend, without importing an ecosystem
Fitz compiles to a native binary and has HTTP/async/Postgres/JWT in the syntax. But a backend eventually needs a UI, and the usual answer is: bolt on the JavaScript ecosystem — a framework (React/Vue/Svelte), a bundler, a node_modules, a second toolchain, and a second definition of every type that crosses the wire.
Fitz's answer is a component format the same compiler understands, that compiles to WebAssembly — no JS framework, no npm.
A component
A .fitzv is a single file: state, events, a <template>, optional scoped styles — the Vue/Svelte shape, but it's Fitz all the way down:
component Counter {
state {
count: Int = 0
}
event inc() { count = count + 1 }
event dec() { count = count - 1 }
<template>
<div class="counter">
<button @click="dec">-</button>
<span class="n">{count}</span>
<button @click="inc">+</button>
</div>
</template>
<style scoped>
.counter { display: flex; gap: 12px; }
.n { font-variant-numeric: tabular-nums; }
</style>
}
{count} interpolates state. @click wires an event. <style scoped> is scoped to this component (via a hash on the class names). Build it:
fitz build --bin web --target wasm-client # → target/wasm/web/{web.js, web_bg.wasm}
That's the whole toolchain. No package.json, no bundler config.
What the compiler emits
There's no framework runtime in the bundle. The emitter lowers the component directly to Rust over wasm-bindgen + web-sys: a struct with the state fields, a mount() that builds the DOM with create_element/append_child, event closures that mutate state, and a render() that re-paints the component's subtree on a state change. It's a self-contained WASM module — the counter above is 26 KB raw / 11.4 KB gzipped.
The template is a real DSL: interpolation ({user.name.upper()} — arbitrary Fitz expressions over state), directives ({#if} / {#for}), events (@click / @input), and composition:
<template>
<ul>
{#for todo in todos}
<TodoRow label="{todo.title}" @remove="drop" />
{/for}
</ul>
</template>
<Child prop="v" /> passes props down; @remove="drop" bubbles an event up with its payload; <slot> lets a parent inject content. Composition works across files, so a component library is just imports.
The type is the same type
This is the part a bolted-on framework can't give you. The type your server defines is the exact one your component uses — imported from the same .fitz module:
// models.fitz
type Todo {
id: Int
title: Str
done: Bool
}
// App.fitzv
from models import Todo
// ... a List<Todo> in state, {todo.title} in the template
The server compiles Todo into a native struct; the component compiles it into a WASM struct; there's one definition. No types.ts that drifts from the backend, no codegen step to keep them in sync — by construction they can't diverge.
Why this is different
Component models aren't new — the point is what compiles them:
-
React / Vue / Svelte are excellent, but they're JavaScript: a framework runtime shipped to the browser, a bundler, an
npmtree, and a separate type story for the API boundary..fitzvcompiles to WASM with none of that. - Elm is a lovely own-language frontend, but it compiles to JavaScript and lives only in the browser — it isn't the same language as your compiled backend.
-
Rust WASM frameworks (Dioxus, Leptos) compile to WASM too, but you're adopting a framework, its macro DSL, and its toolchain on top of Rust. In Fitz the component is the language, and the same
fitzbinary builds it.
One language, one compiler, one type definition — emitting a native binary for the server and a WASM bundle for the browser.
The honest edges (MVP)
- Re-render is naive: a state change re-paints the component's subtree, not a fine-grained signal graph. It's fast enough for the common case; fine-grained reactivity is a future slice.
- The template supports a growing envelope of constructs (interpolation,
{#if}/{#for}, events, composition, slots, scoped styles); genuinely exotic patterns fall outside it for now and the compiler tells you where. - The client is WASM-first; there's no JS-vanilla target — a deliberate choice recorded when the counter came in under the 40 KB bundle gate.
That's the frontend as a first-class part of the language: components you write in Fitz, compiled to WebAssembly, sharing types with a backend compiled to native code. Next up: how a function on that backend becomes callable from the component as if it were local — @rpc.
Fitz is a compiled language with gradual typing and HTTP/async/DB as first-class citizens, compiling to native binary via Rust. Its frontend is .fitzv single-file components compiled to WebAssembly — no framework, no npm. Open source.
Top comments (0)