Skip to main content

Getting Started

tl;dr — generateParseModule(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​