Advanced
Programmatic Invocation
Use CraftClient to dispatch messages into Routecraft routes from any external code -- CLI tools, HTTP handlers, background jobs, or application logic.
When to embed instead of using the CLI
The craft CLI is Bun-only (see the Runtime reference). If your application must run on Node, embed @routecraft/routecraft directly: import the builder, define your routes, and run them inside your existing Node process. No CLI, no Bun.
The library itself works on Node 22.6 or later for runtime type stripping, and is recommended on Node 23.6 or later where stripping is on by default. It also works under Bun if you prefer not to use the CLI for an embedded use case.
Install:
bun add @routecraft/routecraft
Run a Node entry under type stripping:
node --experimental-strip-types runner.ts
(The flag is a no-op on Node 23.6+; type stripping is on by default.)
How it works
When you build a context with ContextBuilder, you get back both the context and a client. The client's send() method dispatches a message to any route that uses a direct() source, runs it through the full route pipeline (transforms, destinations, error handling), and returns the result.
This means you can embed Routecraft as a library inside any application. The routes hold your business logic; the surrounding code handles I/O, user interaction, or HTTP plumbing.
import { ContextBuilder } from '@routecraft/routecraft';
const { context, client } = await new ContextBuilder()
.routes(myRoutes)
.build();
await context.start();
// Dispatch from anywhere
const result = await client.send('greet', { name: 'World' });
Build a CLI
Use Commander (or any CLI framework) to parse arguments, then dispatch into routes via client.send(). This gives you full control over help text, subcommands, and shell completion while keeping business logic in Routecraft routes.
import { Command } from 'commander';
import { direct, craft, noop, ContextBuilder } from '@routecraft/routecraft';
// 1. Define routes using direct() sources
const routes = craft()
.id('greet')
.from(direct())
.transform((body) => `Hello, ${(body as { name: string }).name}!`)
.to(noop())
.id('deploy')
.from(direct())
.transform((body) => {
const { env, dryRun } = body as { env: string; dryRun?: boolean };
if (dryRun) return `Would deploy to ${env}`;
return `Deployed to ${env}`;
})
.to(noop());
// 2. Build context and get the client
const contextBuilder = new ContextBuilder();
contextBuilder.routes(routes);
const { context, client } = await contextBuilder.build();
context.start();
// 3. Wire Commander commands to client.send()
const program = new Command().name('my-tool').version('1.0.0');
program.hook('postAction', async () => {
await context.stop();
});
program
.command('greet')
.description('Greet someone')
.argument('<name>', 'Who to greet')
.action(async (name: string) => {
const result = await client.send('greet', { name });
console.log(result);
});
program
.command('deploy')
.description('Deploy the app')
.requiredOption('-e, --env <env>', 'Target environment')
.option('-d, --dry-run', 'Preview without deploying')
.action(async (opts: { env: string; dryRun?: boolean }) => {
const result = await client.send('deploy', opts);
console.log(result);
});
await program.parseAsync();
my-tool greet Alice # Hello, Alice!
my-tool deploy -e staging -d # Would deploy to staging
my-tool --help # Commander-generated help
Lifecycle
- Call
context.start()before dispatching.direct()sources are ready immediately since they wait for explicitsend()calls. - Stop the context after the CLI command finishes. The
postActionhook in the example above handles this automatically. - For error handling, wrap
client.send()in a try/catch and setprocess.exitCodeas needed.
Embed in a web framework
The same direct() + CraftClient pattern works inside HTTP frameworks. Start the context once when the server boots, then call client.send() from request handlers.
Next.js API route
// lib/routecraft.ts -- shared singleton
import { ContextBuilder, direct, craft, noop } from '@routecraft/routecraft';
const routes = craft()
.id('greet')
.from(direct())
.transform((body) => `Hello, ${(body as { name: string }).name}!`)
.to(noop());
const contextBuilder = new ContextBuilder();
contextBuilder.routes(routes);
const { context, client } = await contextBuilder.build();
await context.start();
export { client };
// app/api/greet/route.ts
import { client } from '@/lib/routecraft';
export async function POST(request: Request) {
const body = await request.json();
const result = await client.send('greet', body);
return Response.json({ message: result });
}
Express
import express from 'express';
import { ContextBuilder, direct, craft, noop } from '@routecraft/routecraft';
const routes = craft()
.id('greet')
.from(direct())
.transform((body) => `Hello, ${(body as { name: string }).name}!`)
.to(noop());
const contextBuilder = new ContextBuilder();
contextBuilder.routes(routes);
const { context, client } = await contextBuilder.build();
await context.start();
const app = express();
app.use(express.json());
app.post('/greet', async (req, res) => {
const result = await client.send('greet', req.body);
res.json({ message: result });
});
app.listen(3000);
Lifecycle tips
- Start the context once at boot, not per-request.
- For graceful shutdown, call
context.stop()in your server's shutdown handler (e.g.,process.on('SIGTERM', ...)). client.send()throws aRoutecraftError(RC5004) if the endpoint does not exist. Return a 404 or appropriate status in your error handler.
Related
direct adapter
The direct() adapter that powers programmatic dispatch.
CLI reference
craft run and other CLI commands.
Configuration
ContextBuilder options and craftConfig.