TypeScript Quick Start
Build an HTTP route matcher with xuma (pure TypeScript) or xuma-crust (WASM-backed).
Install
# Pure TypeScript
bun add xuma
# WASM-backed (faster, same API surface)
bun add xuma-crust
Requires Bun runtime. xuma uses re2js for linear-time regex.
Write a Config
Create routes.yaml:
matchers:
- predicate:
type: and
predicates:
- type: single
input: { type_url: "xuma.http.v1.PathInput", config: {} }
value_match: { Prefix: "/api" }
- type: single
input: { type_url: "xuma.http.v1.MethodInput", config: {} }
value_match: { Exact: "GET" }
on_match: { type: action, action: "api_read" }
- predicate:
type: single
input: { type_url: "xuma.http.v1.PathInput", config: {} }
value_match: { Exact: "/health" }
on_match: { type: action, action: "health" }
on_no_match: { type: action, action: "not_found" }
Validate with the CLI
$ rumi check http routes.yaml
Config valid
Run with the CLI
$ rumi run http routes.yaml --method GET --path /api/users
api_read
$ rumi run http routes.yaml --method GET --path /health
health
$ rumi run http routes.yaml --method DELETE --path /other
not_found
Load in Your App (xuma)
The pure TypeScript implementation loads the same config:
import { RegistryBuilder, registerHttp, type MatcherConfig } from "xuma";
import { HttpRequest } from "xuma/http";
import { parse } from "yaml";
// Build registry with HTTP inputs
const builder = new RegistryBuilder();
registerHttp(builder);
const registry = builder.build();
// Load config
const yaml = await Bun.file("routes.yaml").text();
const config: MatcherConfig = parse(yaml);
const matcher = registry.loadMatcher(config);
// Evaluate
const request = new HttpRequest("GET", "/api/users");
console.assert(matcher.evaluate(request) === "api_read");
Load in Your App (xuma-crust)
The WASM-backed bindings use the same config format:
import { loadHttpMatcher, type HttpMatcher } from "xuma-crust";
// Load config and build matcher in one call
const matcher: HttpMatcher = loadHttpMatcher("routes.yaml");
// Evaluate with method + path
console.assert(matcher.evaluate("GET", "/api/users") === "api_read");
console.assert(matcher.evaluate("DELETE", "/other") === "not_found");
xuma-crust is 3-10x faster than pure TypeScript for evaluation.
Compiler Shorthand
For type-safe HTTP matching without config files:
import { compileRouteMatches, HttpRequest } from "xuma/http";
import type { HttpRouteMatch } from "xuma/http";
const routes: HttpRouteMatch[] = [
{
path: { type: "PathPrefix", value: "/api" },
method: "GET",
},
{
path: { type: "PathPrefix", value: "/admin" },
method: "POST",
},
];
const matcher = compileRouteMatches(routes, "allowed", "denied");
console.assert(matcher.evaluate(new HttpRequest("GET", "/api/users")) === "allowed");
console.assert(matcher.evaluate(new HttpRequest("DELETE", "/api/users")) === "denied");
Within a single HttpRouteMatch, all conditions are ANDed. Multiple routes are ORed. First match wins.
Integration: Bun HTTP Server
import { compileRouteMatches, HttpRequest } from "xuma/http";
const matcher = compileRouteMatches(
[{ path: { type: "PathPrefix", value: "/api" }, method: "GET" }],
"allowed",
"denied",
);
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
const request = new HttpRequest(
req.method,
url.pathname + url.search,
Object.fromEntries(req.headers),
);
if (matcher.evaluate(request) === "denied") {
return new Response("Not found", { status: 404 });
}
return new Response("OK");
},
});
Safety
- ReDoS protection –
re2jsguarantees linear-time regex matching. - Immutable – all types use
readonlyfields. - Depth limits – nested matchers capped at 32 levels.
- Fail-closed – missing data from
DataInputreturnsnull, which makes the predicate evaluate tofalse.
Next Steps
- The Matching Pipeline – how data flows through the matcher
- CLI Reference – all commands and domains
- Config Format – full config schema and type URL tables
- API Reference – generated docs for all languages