API Route
In this guide a single POST /api/records endpoint is backed by a Plexis pipeline whose nodes form the handler chain — validate → service → persist → respond — with forks routing failures to an error-response path. A small request domain tracks the lifecycle state of each request independently.
The handler pipeline
Section titled “The handler pipeline”Each stage of request processing is a pipeline node. Forks decide which path the request follows based on the outcome of the preceding node.
import { definePipeline, node, action, fork, target, terminal } from '@tde.io/plexis';
type RequestCtx = { body: Record<string, unknown>; userId?: string; validated: boolean; recordId?: string; response?: { status: number; body: unknown }; error?: string;};
const handleRequest = definePipeline<RequestCtx>('handle-request', () => { node('validate', () => { action(async (ctx) => { if (!ctx.body.data) return { error: 'Missing required field: data' }; return { userId: ctx.body.userId as string, validated: true }; }); fork('error', target('error-response'), (ctx) => !ctx.validated); fork('next', target('service')); });
node('service', () => { action(async (ctx) => ({ recordId: `rec_${ctx.userId}` })); fork('error', target('error-response'), (ctx) => !!ctx.error); fork('next', target('persist')); });
node('persist', () => { action(async (ctx) => ({})); fork('next', target('respond')); });
node('respond', terminal( async (ctx) => ({ response: { status: 201, body: { id: ctx.recordId, ok: true } }, }) ));
node('error-response', terminal( async (ctx) => ({ response: { status: 400, body: { error: ctx.error ?? 'Bad request' } }, }) ));
return { initial: 'validate' };});The request domain
Section titled “The request domain”A new domain instance is created per request so concurrent requests do not share state. The domain tracks where each request is in its lifecycle independently from the handler logic.
import { defineDomain, when, on, terminal } from '@tde.io/plexis';
type RequestLifecycle = { requestId: string };
function createRequestDomain(requestId: string) { return defineDomain<RequestLifecycle>('request', () => { when('pending', () => { on('start', target('processing')); }); when('processing', () => { on('complete', target('complete')); on('fail', target('failed')); }); when('complete', terminal()); when('failed', terminal());
return { context: { requestId }, initial: 'pending' }; });}Wiring the HTTP server
Section titled “Wiring the HTTP server”The pipeline and domain compose inside a standard Node.js http request handler. Because handleRequest is just a function that accepts a context object and returns a result, the same pipeline plugs into any framework handler — Express, Hono, Fastify — without modification.
import http from 'node:http';
const server = http.createServer(async (req, res) => { if (req.method !== 'POST' || req.url !== '/api/records') { res.writeHead(404).end(); return; }
let raw = ''; for await (const chunk of req) raw += chunk; const body = JSON.parse(raw) as Record<string, unknown>;
const requestDomain = createRequestDomain(crypto.randomUUID()); await requestDomain.follow('start');
const result = await handleRequest.run({ body, validated: false });
const { status, body: resBody } = result.context.response!;
if (status === 201) { await requestDomain.follow('complete'); } else { await requestDomain.follow('fail'); }
res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(resBody));});
server.listen(3000, () => { console.log('Listening on http://localhost:3000');});Start the server:
node dist/server.jsPost a valid request:
curl -X POST http://localhost:3000/api/records \ -H 'Content-Type: application/json' \ -d '{"userId":"alice","data":{"value":42}}'Post an invalid request (missing data):
curl -X POST http://localhost:3000/api/records \ -H 'Content-Type: application/json' \ -d '{"userId":"alice"}'The missing field routes the pipeline to the error-response terminal node, which writes a 400 response. The request domain follows fail and lands on the failed terminal state.