The Component Model: Composable Wasm
This chapter covers the Component Model and WIT, the standard that's making Wasm modules from different languages interoperate at the interface level.
The Problem
Core Wasm modules talk in i32, i64, f32, f64. Everything else, including strings, arrays, and structs, requires conventions that are specific to the tooling that produced the module.
A Wasm module compiled from Rust with wasm-bindgen expects a specific allocation protocol for strings. A module compiled from AssemblyScript expects a different one. A C++ module compiled with Emscripten expects yet another.
You can't consume a wasm-bindgen module from AssemblyScript without recreating the allocation protocol. You can't compose two modules by "linking" their exports together if they disagree on how strings are represented.
The Component Model fixes this. A component is a Wasm module wrapped with a typed interface that any language's tooling understands.
Components vs Modules
A core module is what every previous chapter produced: a .wasm with raw i32 imports and exports.
A component is a .wasm (sometimes .component.wasm) with a higher-level interface described by WIT (Wasm Interface Type). Components can be composed, linked across languages, and hosted in any runtime that understands the Component Model.
Components wrap core modules plus interface metadata. Tools like cargo component handle the wrapping.
WIT: Wasm Interface Type
WIT is the IDL for Wasm components. A .wit file describes the interface your component exposes and imports.
package example:greeter@0.1.0;
interface greeter {
/// A person to greet.
record person {
name: string,
age: u32,
}
/// Greet a person by name.
greet: func(person: person) -> string;
}
world greeter-world {
export greeter;
}
Highlights:
packageidentifies the WIT package (like a namespace).interfacedefines a named set of types and functions.recordis a struct.funcis a function signature.worldnames the full set of exports and imports a component provides or consumes.
Types in WIT
Primitives:
bool, s8, s16, s32, s64, u8, u16, u32, u64, f32, f64, char, string
Compound:
list<T> a dynamically-sized list
option<T> nullable T
result<T, E> success or error
tuple<T, U, ...>
record { field: T, ... } struct
variant { case1, case2(T), ... } sum type
enum { name1, name2, ... } bare enum
flags { one, two, three } bitset
resource Foo { ... } opaque type with methods
Far richer than core Wasm's four primitives. Tools translate WIT types to each language's native equivalents.
Building a Component in Rust
cargo component is the tool. Install:
cargo install cargo-component
Create a project:
cargo component new --lib greeter
cd greeter
wit/world.wit:
package example:greeter@0.1.0;
world greeter {
export greet: func(name: string) -> string;
}
src/lib.rs:
#[allow(warnings)]
mod bindings;
use bindings::Guest;
struct Component;
impl Guest for Component {
fn greet(name: String) -> String {
format!("Hello, {name}!")
}
}
bindings::export!(Component with_types_in bindings);
cargo component build --release produces a .component.wasm that implements the interface.
Running a Component
Wasmtime supports components:
wasmtime run --invoke 'greet("Ada")' greeter.component.wasm
# "Hello, Ada!"
Wasmtime parses the WIT, matches the invocation to the export, handles string marshaling, returns the result.
No glue code on either side. WIT is the contract.
Composition
Two components can be composed when one imports what the other exports. wasm-tools compose does this:
wasm-tools compose \
--definitions consumer.wasm \
--definitions greeter.wasm \
-o composed.wasm
The tool links the consumer's import greeter to the provider's export greeter, producing a single self-contained component.
This is the Component Model's headline promise: cross-language composition. A Rust component providing one interface, a Go component consuming it, a third component wiring them together. All typed, all safe, all compiled down to one Wasm.
WASI Preview 2
WASI Preview 2 is defined entirely in WIT. The standard interfaces look like:
package wasi:cli@0.2.0;
interface run {
run: func() -> result;
}
world command {
export run;
import wasi:filesystem/types;
import wasi:io/streams;
// ...
}
A Preview 2 CLI component exports wasi:cli/run. Runtimes implement the imports. No more POSIX-by-convention; it's a typed contract.
Building Preview 2 CLIs in Rust:
rustup target add wasm32-wasip2
cargo build --target wasm32-wasip2 --release
The binary produced is a component, not a core module. Wasmtime 14+ runs it.
wasi:http
Preview 2's headline interface. A component that imports wasi:http/outgoing-handler and exports wasi:http/incoming-handler is an HTTP server or proxy. Host runtimes (Wasmtime, Fermyon Spin, and others) invoke the component per incoming request.
interface incoming-handler {
handle: func(request: incoming-request, response-out: response-outparam);
}
You write a Rust component that implements handle; the host routes requests to it.
This is what makes Wasm a credible edge compute target. HTTP is standardized, not runtime-specific.
Tooling
The Component Model ecosystem:
cargo component: Cargo extension for building components in Rust.wasm-tools: CLI for inspecting, composing, validating components.jco: JavaScript/TypeScript tooling for components.wit-bindgen: generates bindings for multiple languages from WIT.
Every week adds more tools. The pace is fast.
Adoption Status
As of the time of this tutorial, the Component Model is stabilizing but not universally adopted. Realistic picture:
- Wasmtime: supports it, actively evolving.
- Wasmer, WasmEdge: supports Preview 1, Preview 2 in progress.
- Browser runtimes (V8, SpiderMonkey): core Wasm support, no Component Model yet.
- Hosting platforms (Fermyon Spin, Wasmer Edge): increasingly component-first.
For server-side and edge, the Component Model is where the industry is heading. For browser Wasm, core modules with wasm-bindgen are still the default.
Why It Matters
Three things the Component Model changes.
Language-Agnostic Composition
You can ship a component written in Rust to a JavaScript team, and they can use it through typed JS bindings generated from the same WIT.
Standardized Interfaces
wasi:http, wasi:keyvalue, wasi:cli, and friends give every component a shared vocabulary. No more vendor-specific imports.
Evolution Without Breakage
WIT packages are versioned. A runtime can support wasi:http@0.2.0 and wasi:http@0.3.0 side by side. Components pin a version and keep working.
Common Pitfalls
Mixing core modules and components. You can't just drop a wasm-bindgen module into a Component Model host. Use wasm-tools component new to wrap a core module as a component.
Assuming Preview 2 works everywhere. Some runtimes still prioritize Preview 1. Check support before committing.
Over-engineering interfaces. WIT is expressive; it's also easy to make a 10-record interface when two primitives would do. Keep interfaces minimal.
Treating WIT as a full schema language. It handles data and function signatures, not business rules. Validation still happens in code.
Next Steps
Continue to 11-ecosystem.md for the wider tooling and use cases.