Skip to content

Build Pipeline

TypoKit’s build pipeline is a Rust-native transform that converts plain TypeScript types into optimized runtime artifacts. It’s the core of the framework’s “write the type once, generate everything” philosophy.

TypoKit draws a hard line between build time and runtime:

PhaseLanguageRationale
Build time — AST parsing, type extraction, codegen, route compilation, OpenAPI generation, schema diffingRust (via napi-rs)CPU-intensive computation that benefits from native performance
Runtime — server adapters, middleware, handlers, error handling, logging, debug sidecarTypeScriptAI agents must be able to read, trace, and modify runtime behavior

This follows the pattern established by SWC, Turbopack, Biome, and oxc — Rust for heavy computation, TypeScript for developer interaction. No native code runs in production unless you explicitly opt into a Rust-based server adapter.

Custom TypeScript transforms are notorious for:

  • Breaking with TypeScript version upgrades
  • Not integrating with bundlers (Webpack, Rspack, Vite, esbuild)
  • Causing problems in monorepo tools (Nx, Turborepo)
  • Confusing AI agents that don’t understand the transform step

TS compiler plugins (ttypescript, ts-patch) require patching the TypeScript installation. Instead, TypoKit owns the build step entirely — the transform is a pre-compilation code generation step that reads TypeScript source, generates plain TypeScript files, and lets standard tools compile them normally.

Terminal window
# TypoKit IS the build tool for the server package
typokit build # Production build
typokit dev # Dev mode with watch + debug sidecar

The Rust transform executes eight stages in sequence:

  1. Parse — SWC’s swc_ecma_parser parses TypeScript source files into ASTs. This is the same parser used by SWC/Turbopack, proven to be 20–70× faster than TypeScript’s own parser.

  2. Extract — Type metadata extraction walks the ASTs, pulling out interfaces, JSDoc tags (@table, @format, @minLength, etc.), generic parameters, union types, and route contracts.

  3. Route Table — A compiled radix tree is constructed from the extracted route contracts. The tree structure is serialized to a portable TypeScript module.

  4. OpenAPI — An OpenAPI 3.1 specification is generated from the type metadata, including request/response schemas, parameter definitions, and endpoint documentation.

  5. Test Stubs — Contract test scaffolding is generated for each route, including valid and invalid fixture data derived from the type constraints.

  6. Schema Diff — The current type metadata is compared against the previous build’s snapshot to produce a structured changeset for migration generation.

  7. Typia Callback — For each type needing runtime validation, the Rust transform calls back into Typia’s JavaScript API via napi-rs to generate optimized validator functions (see Typia Integration below).

  8. Write — All generated artifacts are collected and written to the .typokit/ output directory.

src/types.ts (pure TS)
┌─────────────────────────────────┐
│ TypoKit Native Transform │ ← Rust via napi-rs
│ 1. Parse TS ASTs │
│ 2. Extract type metadata │
│ 3. Generate route table │
│ 4. Generate OpenAPI spec │
│ 5. Generate test stubs │
│ 6. Schema diff (migrations) │
│ 7. Validator codegen │
│ (Typia callback to JS) │
│ 8. Write to .typokit/ │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Standard TS Compile │ ← user's existing toolchain
│ (tsc / tsup / SWC) │
│ Output: dist/ │
└─────────────────────────────────┘

Typia is a TypeScript transformer that handles hundreds of edge cases in validation — union discrimination, template literals, mapped types, conditional types, recursive types. Rather than rewriting Typia’s logic in Rust, TypoKit uses a callback pattern across the napi-rs boundary:

Rust Transform JavaScript (Typia)
───────────── ──────────────────
1. Parse TS ASTs ← Rust
2. Extract type metadata ← Rust
3. Generate route table ← Rust
4. Generate OpenAPI spec ← Rust
5. Generate test stubs ← Rust
6. Schema diff ← Rust
7. For each type needing validation:
├── Pass type metadata ──────────→ Receive metadata
│ Call Typia's API
└── Receive validator code ←────── Return generated code
8. Write all outputs ← Rust

Steps 1–6 and 8 are where the heavy computation lives (AST parsing and traversal), running at native speed. Step 7 crosses the JS boundary only for validation codegen — a fraction of the total work.

The native transform is published as prebuilt binaries via napi-rs:

@typokit/transform-native
├── src/ # Rust source
│ ├── parser.rs # TS AST parsing (swc_ecma_parser)
│ ├── type_extractor.rs # JSDoc + type metadata extraction
│ ├── route_compiler.rs # Radix tree construction + serialization
│ ├── openapi_generator.rs # OpenAPI 3.1 spec generation
│ ├── schema_differ.rs # Type diff for migration generation
│ ├── test_stub_generator.rs # Contract test scaffolding
│ └── typia_bridge.rs # napi-rs callback to Typia JS API
├── npm/ # Prebuilt platform packages
│ ├── darwin-arm64/ # macOS Apple Silicon
│ ├── darwin-x64/ # macOS Intel
│ ├── linux-arm64-gnu/ # Linux ARM64
│ ├── linux-x64-gnu/ # Linux x64
│ ├── linux-x64-musl/ # Alpine / Docker
│ └── win32-x64-msvc/ # Windows x64
└── index.js # Loads correct platform binary

npm install automatically selects the right binary for the current platform. The linux-x64-musl target ensures compatibility with Alpine-based Docker images common in CI.

All generated artifacts land in .typokit/:

.typokit/
validators/
User.validator.ts # Generated validation function (via Typia)
CreateUserInput.validator.ts
routes/
route-table.ts # Compiled route registry
compiled-router.ts # Compiled radix tree
schemas/
openapi.json # Generated OpenAPI spec
tests/
users.contract.test.ts # Generated contract tests
client/
index.ts # Generated type-safe client

This directory is:

  • Gitignored — generated on build, never committed
  • Inspectable — AI agents can read all outputs (everything is TypeScript/JSON, not Rust artifacts)
  • Cacheable — only regenerated when source types change (content-hash based)

The build pipeline is structured as a tapable hook pipeline inspired by Rspack/Webpack’s plugin architecture. Hooks are exposed as a TypeScript API — plugin authors write hooks in TypeScript, and the Rust transform calls them at the appropriate points via napi-rs.

// @typokit/core/src/build/pipeline.ts
export interface BuildPipeline {
hooks: {
/** Runs before any transforms — register additional type sources */
beforeTransform: AsyncSeriesHook<[BuildContext]>;
/** Runs after types are parsed — inspect/modify the type map */
afterTypeParse: AsyncSeriesHook<[SchemaTypeMap, BuildContext]>;
/** Runs after validators are generated — add custom validators */
afterValidators: AsyncSeriesHook<[GeneratedOutput[], BuildContext]>;
/** Runs after the route table is compiled */
afterRouteTable: AsyncSeriesHook<[CompiledRouteTable, BuildContext]>;
/** Runs after all generation — plugins emit their own artifacts */
emit: AsyncSeriesHook<[GeneratedOutput[], BuildContext]>;
/** Override or extend the compilation step */
compile: AsyncSeriesHook<[CompileContext, BuildContext]>;
/** Runs after build completes — cleanup, reporting */
done: AsyncSeriesHook<[BuildResult]>;
};
}

The seven phases execute in order:

  1. beforeTransform — Pre-processing, setup, register additional type sources
  2. afterTypeParse — Type metadata extracted and available for inspection or modification
  3. afterValidators — Compiled validators generated, plugins can add custom validators
  4. afterRouteTable — Radix tree route table compiled
  5. emit — All artifacts ready, plugins emit their own files to .typokit/
  6. compile — Compilation step. By default runs tsc, but plugins can override this (e.g., plugin-axum runs cargo build instead). The CompileContext.handled flag prevents the default compiler from running.
  7. done — Build complete, cleanup and reporting

Here’s how @typokit/plugin-ws uses hooks to generate WebSocket artifacts:

// @typokit/plugin-ws/src/build.ts
import type { TypoKitPlugin } from "@typokit/core";
export const wsPlugin: TypoKitPlugin = {
name: "@typokit/plugin-ws",
onBuild(pipeline) {
// After types are parsed, extract WebSocket channel contracts
pipeline.hooks.afterTypeParse.tap("ws-plugin", (typeMap, ctx) => {
const wsChannels = extractWsChannels(typeMap);
ctx.set("wsChannels", wsChannels);
});
// At emit phase, generate WS handler types and validators
pipeline.hooks.emit.tap("ws-plugin", (outputs, ctx) => {
const wsChannels = ctx.get("wsChannels");
if (wsChannels.length > 0) {
outputs.push(
generateWsValidators(wsChannels),
generateWsRouteTable(wsChannels),
);
}
});
},
};

The compile hook receives a CompileContext that plugins use to override or extend the default TypeScript compilation:

export interface CompileContext {
/** Set to true if your plugin handled compilation — skips the default tsc step */
handled: boolean;
/** Which compiler ran (e.g., "tsc", "cargo") */
compiler?: string;
/** Compilation result */
result?: { success: boolean; errors: string[] };
}

For example, @typokit/plugin-axum uses the compile hook to run cargo build instead of tsc:

pipeline.hooks.compile.tap("plugin-axum", async (compileCtx, ctx) => {
const result = spawnSync("cargo", ["build"], { cwd: ctx.rootDir });
compileCtx.handled = true; // Skip default tsc
compileCtx.compiler = "cargo";
compileCtx.result = {
success: result.status === 0,
errors: result.status !== 0 ? [result.stderr] : [],
};
});
  • Precise ordering — plugins run at exact points in the pipeline, no ambiguity
  • Typed context — each hook receives typed data, so AI agents can reason about what’s available at each phase
  • Deterministic execution — hooks are async-series by default, no race conditions
  • Inspectabletypokit inspect build-pipeline --json shows all registered hooks and their order

TypoKit uses content-hash caching for incremental rebuilds. Each source file’s content is hashed, and the transform only regenerates artifacts when the hash changes.

In typokit dev mode, the native transform runs incrementally:

  • File watcher detects changes to types.ts types
  • Rust transform re-parses only changed files (AST cache held in memory)
  • Only affected validators and routes are regenerated (dependency graph tracked in Rust)
  • Hot reload applies changes without full restart
  • Debug sidecar stays running across reloads
MetricTargetComparison (pure TS approach)
Cold build (50 types, 20 routes)< 500ms~2–4s with ts-morph
Cold build (200 types, 100 routes)< 2s~10–15s with ts-morph
Incremental rebuild (1 type changed)< 50ms~500ms–1s with ts-morph
Hot reload (dev mode, 1 file)< 50ms~200ms with ts-morph
Memory usage (200 types)< 100MB~300–500MB with ts-morph

These targets are achievable because the Rust transform uses SWC’s parser (proven 20–70× faster than TypeScript’s own parser) and performs all AST traversal, metadata extraction, and code generation in native memory without JS garbage collection pressure.

TypoKit integrates as a build command, not a plugin:

{
"scripts": {
"build": "typokit build",
"dev": "typokit dev"
}
}

The Nx executor and Turborepo integration are thin wrappers that call typokit build with the correct working directory. No custom Webpack/Rspack loaders. No plugin configuration. One command that works everywhere.