Messaging
You can add messaging capabilities to a component with the wasmcloud:messaging interface.
This guide walks through the wasmCloud http-api-with-distributed-workloads template, which demonstrates how to dispatch work from an HTTP API to a background component via a message broker. You can use this guide as a model for implementing wasmcloud:messaging in your own components.
Overview
Messaging typically spans at least two components. A component can import consumer to publish events without a handler in the same deployment; for example, the component might publish audit events that an external NATS subscriber consumes. A handler can also receive from multiple senders.
The example template used on this page demonstrates a two-component request-reply pattern that may serve as a starting point. The template consists of:
http-api— An HTTP component that accepts aPOST /taskrequest and useswasmcloud:messaging/consumerto dispatch a request message and await its reply.task-worker— A headless component that exportswasmcloud:messaging/handlerto receive messages, process them, and publish a reply.
wasmcloud:messaging uses NATS as its production transport, built into the wasmCloud runtime. During development with wash dev, messaging is handled in-process; no NATS server is required. In production, the runtime connects to NATS automatically when the host starts with a NATS URL.
Adding wasmcloud:messaging in this component project touches four files: two WIT worlds and two bundler configs:
http-api/wit/world.wit— Declares the messaging consumer import in the HTTP componenttask-worker/wit/world.wit— Declares the consumer import and handler exporthttp-api/rolldown.config.mjs— Extends the externals list to coverwasmcloud:importstask-worker/rolldown.config.mjs— Same change as above
Plus the two TypeScript implementation files:
http-api/src/component.ts— Usesrequest()to dispatch tasks and receive repliestask-worker/src/component.ts— Exports ahandlerobject withhandleMessage()
A full working example is available as a template:
wash new https://github.com/wasmCloud/typescript.git \
--name http-api-with-distributed-workloads \
--subfolder templates/http-api-with-distributed-workloadsStep 1: Declare the WIT worlds
Every capability a component uses or provides must be declared in its WIT world. This section covers:
- The
http-apiWIT world — importsconsumeronly - The
task-workerWIT world — importsconsumer, exportshandler - What each interface provides
- How
wasmcloud:messagingWIT dependencies are resolved
HTTP API — import the consumer
The http-api component only sends messages; it never receives them. In http-api/wit/world.wit, declare a single import:
package wasmcloud:templates@0.1.0;
world typescript-http-api-with-distributed-workloads-api {
import wasmcloud:messaging/consumer@0.2.0;
export wasi:http/incoming-handler@0.2.6;
}Task worker — import consumer, export handler
The task-worker component receives messages and sends replies. Receiving requires exporting handler; replying requires importing consumer. In task-worker/wit/world.wit:
package wasmcloud:templates@0.1.0;
world typescript-http-api-with-distributed-workloads-worker {
import wasmcloud:messaging/consumer@0.2.0;
export wasmcloud:messaging/handler@0.2.0;
}What these interfaces provide
| Interface | Direction | Purpose |
|---|---|---|
wasmcloud:messaging/consumer | import | Send messages (request, publish) |
wasmcloud:messaging/handler | export | Receive messages (handleMessage) |
The underlying message type used by both interfaces:
record broker-message {
subject: string,
body: list<u8>, // Uint8Array in TypeScript
reply-to: option<string>,
}How dependency resolution works
When you run npm run build, the build pipeline calls wash wit fetch as a setup step. wasmcloud:messaging is hosted at the wasmcloud.com package registry — distinct from the wasi.dev registry used for standard WASI interfaces. Both registries are resolved automatically; no extra configuration is required.
Step 2: Update rolldown.config.mjs
Unlike wasi: imports, wasmcloud: imports are not covered by the default external pattern in rolldown.config.mjs. Both components need this pattern extended.
Before (default in single-component templates):
- external: /wasi:.*/,After — http-api/rolldown.config.mjs (required for any component using wasmcloud: interfaces):
import { defineConfig } from "rolldown";
export default defineConfig({
input: "src/component.ts",
external: [/wasi:.*/, /wasmcloud:.*/],
output: {
file: "dist/component.js",
format: "esm",
},
});Apply the same change to task-worker/rolldown.config.mjs. Without this, rolldown will attempt to bundle the wasmcloud:messaging/consumer module and fail with a "could not resolve" error.
Step 3: Use request() in the HTTP API
With the WIT world updated, you can import messaging functions in TypeScript using the exact WIT interface name as the import path.
Import
In http-api/src/component.ts:
// @ts-expect-error — types generated after running npm run setup:wit
import { request } from 'wasmcloud:messaging/consumer@0.2.0';Why @ts-expect-error? The import path wasmcloud:messaging/consumer@0.2.0 is a bare module specifier that TypeScript can't resolve against the project's type definitions at compile time, so it reports a type error. The directive suppresses that. The import itself works correctly at runtime: rolldown marks it as external, and jco componentize wires it to the real interface when building the Wasm component.
Once you've run npm run setup:wit, the generated signatures are available in generated/types/interfaces/wasmcloud-messaging-consumer.d.ts for reference:
declare module 'wasmcloud:messaging/consumer@0.2.0' {
export function request(subject: string, body: Uint8Array, timeoutMs: number): BrokerMessage;
export function publish(msg: BrokerMessage): void;
export type BrokerMessage = import('wasmcloud:messaging/types@0.2.0').BrokerMessage;
}Calling request()
request() implements the request-reply pattern: it publishes body to subject, blocks until a reply arrives (up to timeoutMs milliseconds), and returns the reply message. It is synchronous, with no await required.
On error (timeout, no subscriber, or delivery failure) request() throws rather than returning. Always wrap it in try/catch. In http-api/src/component.ts:
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const REQUEST_TIMEOUT_MS = 5000;
app.post('/task', async (c) => {
const body = await c.req.json<{ worker?: string; payload: string }>();
const subject = `tasks.${body.worker ?? 'default'}`;
const msgBody = encoder.encode(body.payload);
try {
const reply = request(subject, msgBody, REQUEST_TIMEOUT_MS);
return c.text(decoder.decode(reply.body));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return c.text(`Messaging error: ${message}`, 502);
}
});If no component is subscribed to subject within timeoutMs milliseconds, request() throws with a timeout error. During wash dev, the wasmCloud runtime routes calls in-process, so timeouts only occur if the handler throws or takes too long. In production, a missing NATS subscription or an unreachable server both surface as timeout errors.
How import paths map to WIT interfaces
The import path is always the fully-qualified WIT interface name:
wasmcloud:messaging/consumer@0.2.0 → import { request, publish } from 'wasmcloud:messaging/consumer@0.2.0'
Subject naming
Subjects are arbitrary dot-separated strings following NATS subject conventions. The template uses tasks.{worker} so that multiple worker types can each subscribe to their own sub-subject:
tasks.task-worker → handled by the leet-speak task-worker component
tasks.summarizer → would be handled by a hypothetical summarizer component
In development with wash dev, all messages are routed in-process regardless of subject; the subject only becomes meaningful in production where NATS subscription patterns control routing.
Step 4: Implement handleMessage() in the task worker
The task-worker exports wasmcloud:messaging/handler@0.2.0. Exporting a WIT interface is different from importing one: instead of calling a function, you provide an object that the runtime calls into.
The handler export
componentize-js maps export wasmcloud:messaging/handler@0.2.0 in the WIT world to a named JavaScript export. The export name is the segment of the interface path between / and @:
wasmcloud:messaging/handler@0.2.0
^^^^^^^ → export name: handler
That export must be an object with a handleMessage method. You cannot rename it. In task-worker/src/component.ts:
export const handler = {
handleMessage(msg: BrokerMessage): void {
// process msg and optionally publish a reply
},
};Replying to a message
The incoming message's replyTo field holds the subject the original caller is listening on. Publish your response there using publish().
BrokerMessage is declared locally as an interface because the generated types use a module augmentation syntax that TypeScript can't resolve at the import site — the same situation as the @ts-expect-error on the consumer import. The local interface gives you type safety without fighting the toolchain. In task-worker/src/component.ts:
// @ts-expect-error — types generated after running npm run setup:wit
import { publish } from 'wasmcloud:messaging/consumer@0.2.0';
interface BrokerMessage {
subject: string;
body: Uint8Array;
replyTo?: string;
}
const encoder = new TextEncoder();
const decoder = new TextDecoder();
export const handler = {
handleMessage(msg: BrokerMessage): void {
if (!msg.replyTo) {
throw new Error('missing reply_to — cannot send response');
}
const payload = decoder.decode(msg.body);
const result = processPayload(payload);
publish({
subject: msg.replyTo,
body: encoder.encode(result),
});
},
};publish() is fire-and-forget: it delivers the message and returns. Like request(), it throws on delivery error.
To signal a handler failure, throw from handleMessage. The thrown error propagates back to the caller: the wasmCloud runtime logs it, and if the sender called request(), the throw surfaces there as a caught error.
Fire-and-forget handlers
If the handler doesn't need to reply (one-way notification pattern), omit the publish() call:
export const handler = {
handleMessage(msg: BrokerMessage): void {
const event = JSON.parse(decoder.decode(msg.body));
processEvent(event); // side effects only, no reply
},
};Step 5: Configure multi-component development
A multi-component project has a root package.json workspace and a root .wash/config.yaml. The component_path in the config points to the primary component (the HTTP API); additional components are declared under dev: components:. In .wash/config.yaml:
new:
command: npm install
build:
command: npm run build
component_path: http-api/dist/http_api.wasm
dev:
components:
- name: task-worker
file: task-worker/dist/task_worker.wasmThe dev: components: list tells wash dev to deploy the task-worker alongside the main component. wash dev automatically links both components through the wasmCloud runtime; no NATS server is required needed during development.
The root package.json coordinates the build across both sub-packages:
{
"scripts": {
"build": "npm run build --workspace=http-api && npm run build --workspace=task-worker",
"dev": "nodemon"
},
"workspaces": ["http-api", "task-worker"]
}npm run dev runs nodemon, which watches source files and re-runs npm run build on changes. It handles the build side of the development loop only. Use wash dev to start the full environment: build, deploy, and serve.
Dependencies
The task-worker has no runtime dependencies, only devDependencies. It doesn't need hono or @bytecodealliance/jco-std because it doesn't handle HTTP. All messaging plumbing comes from the WIT bindings at componentization time. In task-worker/package.json:
{
"devDependencies": {
"@bytecodealliance/jco": "^1.15.4",
"rimraf": "^6.1.2",
"rolldown": "^1.0.0-beta.47",
"typescript": "^5.9.3"
}
}Build and verify
Build
npm install
npm run buildThis runs the full pipeline for both components:
wash wit fetch— downloadswasmcloud:messagingWIT definitions intowit/deps/jco guest-types— generates TypeScript type definitions ingenerated/types/rolldown— bundles TypeScript intodist/component.js(leavingwasi:andwasmcloud:imports external)jco componentize— compilesdist/component.jsinto a.wasmcomponent
Run
wash devwash dev builds both components, starts an HTTP server on port 8000, and routes messaging calls between them in-process.
Test
Open http://localhost:8000 to use the web UI, or test with curl:
# Send a task — the task-worker converts the payload to leet speak
curl -s -X POST http://localhost:8000/task \
-H 'Content-Type: application/json' \
-d '{"worker": "task-worker", "payload": "Hello World"}'
# => H3110 W0r1d
# Missing payload returns 400
curl -s -X POST http://localhost:8000/task \
-H 'Content-Type: application/json' \
-d '{}'
# => Missing required field: payloadProduction deployment
Both components run on the same wasmCloud host. Start the host with a NATS URL; the runtime's built-in messaging plugin connects to NATS automatically. Deploy both components together with a WorkloadDeployment manifest:
apiVersion: runtime.wasmcloud.dev/v1alpha1
kind: WorkloadDeployment
metadata:
name: http-api-with-distributed-workloads
spec:
replicas: 1
template:
spec:
hostInterfaces:
- namespace: wasi
package: http
interfaces:
- incoming-handler
config:
host: your-domain.example.com # HTTP Host header used for routing
- namespace: wasmcloud
package: messaging
interfaces:
- consumer
- handler
config:
subscriptions: "tasks.>" # NATS subjects the task-worker subscribes to
components:
- name: http-api
image: <registry>/http_api:latest
- name: task-worker
image: <registry>/task_worker:latestKey points about the manifest:
hostInterfacesdeclares which built-in host capabilities the workload needs. No separate HTTP server or NATS component is listed — both are provided by the runtime.interfaces: [consumer, handler]declares both the sending (consumer) and receiving (handler) sides of the messaging interface. The runtime automatically wires subscriptions to the component that exportshandler(task-worker), not to http-api which only importsconsumer.subscriptionsis a comma-separated list of NATS subject patterns.tasks.>matches any subject starting withtasks., covering any number of worker types.- HTTP
hostconfig is the Host header the runtime uses to route incoming HTTP requests to this workload. The actual listen address is set at host startup (--http-addr).
Because NATS subscriptions are configured per-workload at deploy time, subject naming becomes a contract between sender and handler. The tasks.{worker} convention in this template means each worker type can be independently scaled and replaced without modifying the HTTP API; just change the subscription on the new workload's manifest.
For Kubernetes deployment, see the runtime-operator documentation.
Summary: checklist for adding wasmcloud:messaging
For a component that sends messages (consumer):
- Add
import wasmcloud:messaging/consumer@0.2.0towit/world.wit. - Add
wasmcloud:to rolldown externals:external: [/wasi:.*/, /wasmcloud:.*/]. - Run
npm run setup:witonce to download WIT definitions and generate type stubs (jco guest-types). - Import with
@ts-expect-error:import { request, publish } from 'wasmcloud:messaging/consumer@0.2.0'. - Use
request(subject, body, timeoutMs)for request-reply; wrap in try/catch since it throws on error or timeout. - Use
publish(msg)for fire-and-forget delivery with no reply expected.
For a component that handles messages (handler):
- Add both
import wasmcloud:messaging/consumer@0.2.0andexport wasmcloud:messaging/handler@0.2.0towit/world.wit. The import is needed forpublish()(replies); the export declares the handler. - Add
wasmcloud:to rolldown externals (same as above). - Export a
handlerobject with ahandleMessagemethod:typescriptexport const handler = { handleMessage(msg: BrokerMessage): void { ... } }; - Use
msg.replyToto find the reply subject for request-reply patterns; callpublish({ subject: msg.replyTo, body })to respond. - Throw to signal errors — the thrown value is returned as the WIT
resulterror variant. - No runtime npm dependencies needed — the handler component only needs dev tooling.
For multi-component development:
- Declare
dev: components:in root.wash/config.yamlwith the path to each additional component's.wasmfile. wash devroutes messages in-process — no NATS server required during development.- In production, use a
WorkloadDeploymentmanifest with awasmcloud:messaginghostInterface entry. Includehandlerin the interfaces list and setsubscriptionsin theconfigblock.
API reference: wasmcloud:messaging functions used
| Function | Signature | Description |
|---|---|---|
request(subject, body, timeoutMs) | (string, Uint8Array, number) => BrokerMessage | Publish to subject and block until a reply arrives (or timeout elapses). Throws on error. |
publish(msg) | (BrokerMessage) => void | Fire-and-forget publish. Throws on delivery error. |
handler.handleMessage(msg) | (BrokerMessage) => void | Called by the runtime for each delivered message. Throw to signal failure. |
BrokerMessage fields:
| Field | Type | Description |
|---|---|---|
subject | string | The message subject (NATS topic) |
body | Uint8Array | Raw message payload |
replyTo | string | undefined | Reply subject set automatically by request() |
Further reading
- TypeScript Language Guide — toolchain overview, HTTP patterns, framework integration, and library compatibility
- Key-Value Storage — Persistent state for a single component
- Blob Storage — Object storage for larger payloads
- Language Support overview — summary of all supported languages and toolchains