Skip to main content

Getting Started

tl;drgenerateParseModule(types: string) returns a JS module source string. A Worker Loader mounts that source as a module; a DO facet loads the module's ParserValidator class; your supervisor DO calls facet.parse(value, typeName) per request. Generate once per schema version and reuse on each request.


Install

npm install @lumenize/ts-runtime-parser-validator

Set up wrangler.jsonc with a Worker Loader binding and a Durable Object for your supervisor:

{
"compatibility_date": "2026-04-01",
"compatibility_flags": ["nodejs_compat"],
"worker_loaders": [
{ "binding": "LOADER" }
],
"durable_objects": {
"bindings": [
{ "name": "SUPERVISOR", "class_name": "SupervisorDO" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["SupervisorDO"] }
]
}

The three-step flow

1. Generate the parse module

Write your schema once, in a regular .d.ts file (full editor support, real type-checking, zero DSL):

// schema.d.ts
interface Address {
street: string;
city: string;
/** @default "US" */
country?: string;
}

interface User {
name: string;
home: Address;
}

Call generateParseModule() once per schema version with the raw source. Store the returned string keyed by a bundle ID (a content hash, a version number, or a tenant ID — whatever fits your lifecycle).

import { generateParseModule } from '@lumenize/ts-runtime-parser-validator';
import schemaTypes from './schema.d.ts?raw';

const moduleSource = generateParseModule(schemaTypes);
// Persist moduleSource by bundleId (KV, DO storage, etc.).

The emitted module has zero runtime dependency on typia — everything needed was inlined during generation.

2. Load the module as a DO facet

Inside your supervisor DO, use getParserValidatorFacet() to mount the generated module as a DO facet. The helper wraps the Worker Loader + facet setup. Your per-request code only supplies bundleId and a callback that returns the moduleSource — the callback only runs on a cold Worker build, so per-request calls skip it entirely, avoiding the associated "created daily" charges.

import { DurableObject } from 'cloudflare:workers';
import {
getParserValidatorFacet,
type ParseResult,
} from '@lumenize/ts-runtime-parser-validator';

export class SupervisorDO extends DurableObject<Env> {
async parse(bundleId: string, value: unknown, typeName: string): Promise<ParseResult> {
const facet = getParserValidatorFacet(
this.ctx,
this.env.LOADER,
bundleId,
() => this.ctx.storage.kv.get(`parser:${bundleId}`) as string,
);
return await facet.parse(value, typeName);
}

// ...

registerModuleSource(bundleId: string, moduleSource: string) {
this.ctx.storage.kv.put(`parser:${bundleId}`, moduleSource);
}
}

Two things worth knowing:

  • bundleId identifies this particular generated module. Re-use the same ID to re-use the same cached Worker and facet; change it to swap in a new validator.
  • Where moduleSource lives is your choice. The callback returns it from wherever fits your lifecycle — DO storage (above), a KV namespace binding, R2, an RPC to a coordinator service. The callback can be sync (as above, where ctx.storage.kv.get is sync) or async — the helper awaits whatever you return. It only runs when Cloudflare needs to (re)build the Worker for this bundleId.

3. Call parse() and handle the result

Valid input comes back with @default values filled in — including nested defaults (country on the embedded Address):

const ok = await supervisor.parse(bundleId, {
name: 'Alice',
home: { street: '1 Main', city: 'Springfield' },
}, 'User');
expect(ok).toEqual({
valid: true,
data: {
name: 'Alice',
home: { street: '1 Main', city: 'Springfield', country: 'US' },
},
});

Invalid input returns typia's structured error list:

const bad = await supervisor.parse(bundleId, {
name: 42,
home: { street: '1 Main', city: 'Springfield' },
}, 'User');
expect(bad).toMatchObject({
valid: false,
errors: [
{ path: '$input.name', expected: 'string', value: 42 },
],
});

Values cross via Workers RPC (structured-clone semantics), so Date, Map, Set, RegExp, TypedArray, and cyclic references all survive the boundary.

On success, data is the input with any @default values filled in. On failure, errors is one entry per failing field, with JSON-pointer-like paths ($input.home.city). See API Reference for the full ParseResult and ValidationError shapes.

Next steps