Documentation
Polyglot WASM Handlers
@kynetra/fx-wasm lets you plug any WebAssembly module — Rust, Go, C++, or any other language that compiles to WASM — directly into a Kynetra FX route or middleware pipeline. The guest module communicates with the host over a lightweight JSON protocol, keeping the ABI simple enough to implement in any language without FFI bindings.
Overview#
Traditional polyglot setups require a separate microservice per language, adding a network hop and operational overhead. WASM handlers collapse that to zero: the guest module runs in the same process as the framework, sandboxed by the WASM runtime. The host serializes the incoming HTTP request as JSON, calls the guest's handle export, and deserializes the JSON response back into a Response object.
wasmHandler(source)— wraps a guest object as a route handler. The framework callssource.handle(requestJson)and returns the result as an HTTP response.wasmMiddleware(source)— wraps a guest object as pipeline middleware. The guest can either return a response to short-circuit the pipeline, or return{ "next": true }to let the next handler run.
Both functions accept the same source shape: any object (or factory function that returns an object) that has a handle(requestJson: string): string | Promise<string> method. A real wasm-pack module, a Rust-compiled WASM instance, and a plain TypeScript object all satisfy this contract — which makes unit testing trivial.
Tip
The guest contract#
The entire ABI is two JSON shapes. The host serializes the incoming request into a WasmRequest string and passes it to handle(). The guest deserializes it, does its work, and returns a WasmResponse string. Both shapes use only primitive types so they map cleanly to every target language.
WasmRequest
| Name | Type | Description |
|---|---|---|
| method | string | HTTP method in upper-case: "GET", "POST", "PATCH", etc. |
| url | string | Full request URL including scheme, host, path, and query string. |
| path | string | URL pathname only, without query string. Equivalent to new URL(url).pathname. |
| query | Record<string, string> | Parsed query parameters as a flat key/value map. Multi-value keys take the last value. |
| headers | Record<string, string> | Request headers as a flat key/value map with lower-cased header names. |
| body | string | null | Request body as a UTF-8 string, or null for methods that carry no body (GET, HEAD, DELETE). |
WasmResponse
| Name | Type | Description |
|---|---|---|
| status | number (optional) | HTTP status code to return. Defaults to 200 when omitted. |
| headers | Record<string, string> (optional) | Response headers to set. Merged with any headers the framework adds. |
| body | string (optional) | Response body as a UTF-8 string. Set content-type in headers to match the body format. |
| next | boolean (optional) | Middleware only. When true the response fields are ignored and the pipeline continues to the next handler. |
Note
next field is only meaningful when using wasmMiddleware(). Returning { "next": true } from a wasmHandler() guest is treated as an empty 200 response.wasmHandler — route handler#
Use wasmHandler() to register a guest module as the sole handler for a route. The example below uses a plain TypeScript object — the same interface a real WASM module implements via its exported handle function.
import { createFX } from '@kynetra/fx'import { wasmHandler, wasmMiddleware } from '@kynetra/fx-wasm' const app = createFX() // Fake guest — same interface a real WASM module implementsconst echoGuest = { handle(requestJson: string): string { const req = JSON.parse(requestJson) return JSON.stringify({ status: 200, headers: { 'content-type': 'application/json' }, body: JSON.stringify({ echo: req.path, method: req.method }), }) },} app.get('/echo', wasmHandler(echoGuest))wasmHandler() calls source.handle() with the serialized WasmRequest and passes the parsed WasmResponse straight to ctx.body(). If the guest throws or returns malformed JSON, the framework responds with 502 FX_WASM_ERROR rather than crashing the process.
wasmMiddleware — pipeline middleware#
Use wasmMiddleware() when the guest should run as part of the middleware pipeline — for example to perform authentication, rate limiting, or request validation in a language other than TypeScript. The guest signals whether to continue the pipeline by returning { "next": true }.
const authCheckGuest = { handle(requestJson: string): string { const req = JSON.parse(requestJson) const auth = req.headers['authorization'] ?? '' if (auth.startsWith('Bearer ')) { // Let the pipeline continue return JSON.stringify({ next: true }) } return JSON.stringify({ status: 401, body: JSON.stringify({ error: { code: 'FX_UNAUTHENTICATED', message: 'Missing token' } }), }) },} app.use(wasmMiddleware(authCheckGuest))app.get('/protected', (ctx) => ctx.json({ secret: 42 }))When the guest returns any response without next: true, the framework stops the pipeline and sends that response to the client. Downstream handlers — including the route handler — are not called.
Tip
next: true) can still set response headers by returning { "next": true, "headers": { "x-guest-id": "rust-v1" } }. These headers are merged into the context before next() is called.Error handling#
The WASM bridge catches all guest-side failures and normalises them into a structured error response so a misbehaving module cannot crash the worker process or leak an unformatted stack trace to the client.
502 FX_WASM_ERROR— the guest threw an exception, returned a non-string value, or returned a string that could not be parsed as valid JSON. The original error is logged atwarnlevel with awasmErrorfield.502 FX_WASM_TIMEOUT— the guest'shandle()promise did not resolve within the configured timeout (default: 5 000 ms). Set{ timeoutMs: number }in the options to adjust.
// Guest that deliberately errors — framework responds 502 FX_WASM_ERRORconst faultyGuest = { handle(_requestJson: string): string { throw new Error('something went wrong inside the guest') },} app.get('/risky', wasmHandler(faultyGuest))// GET /risky → 502 { error: { code: 'FX_WASM_ERROR', message: '...' } }Warning
Using a real wasm-pack module#
When your Rust module is compiled with wasm-pack, the generated JavaScript bindings expose the exported function as a named export. Import the init function and the handler export, call init() once at startup, then wrap the export in a guest object.
import init, { handle } from './pkg/my_module'import { wasmHandler } from '@kynetra/fx-wasm' // Call init() once — it compiles the WASM binary and sets up memoryawait init() const wasmGuest = { handle } app.get('/wasm', wasmHandler(wasmGuest))Rust guest example
The following Rust snippet shows the minimal implementation of the guest contract using the serde_json crate. Compile with wasm-pack build --target web.
use serde::{Deserialize, Serialize};use wasm_bindgen::prelude::*; #[derive(Deserialize)]struct WasmRequest { method: String, path: String,} #[derive(Serialize)]struct WasmResponse { status: u16, body: String,} #[wasm_bindgen]pub fn handle(request_json: &str) -> String { let req: WasmRequest = serde_json::from_str(request_json).unwrap(); let resp = WasmResponse { status: 200, body: format!(r#"{{"path":"{}","method":"{}"}}"#, req.path, req.method), }; serde_json::to_string(&resp).unwrap()}Note
pkg/ directory containing the compiled .wasm binary and JavaScript bindings. Import from ./pkg/my_module (or the package name you set in Cargo.toml). The init() call fetches and compiles the binary — do this once at module load time, not per-request.Factory sources#
Instead of a plain object, wasmHandler() and wasmMiddleware() also accept a factory function. The factory is called once at registration time and the returned guest object is reused for every request. This is useful when the guest needs async initialisation (such as fetching a WASM binary from R2 or KV).
import { wasmHandler } from '@kynetra/fx-wasm' async function buildGuest() { const binary = await fetch('https://assets.example.com/module.wasm').then((r) => r.arrayBuffer()) const { instance } = await WebAssembly.instantiate(binary, {}) const handle = instance.exports.handle as (s: string) => string return { handle }} // Factory receives the FX context at registration timeapp.get('/dynamic', wasmHandler(await buildGuest()))Note