Node.js — gateway routing
Path-level ownership on any Node version: the main thread parses HTTP once and proxies matched URL prefixes to worker-owned internal listeners. Some routes in worker A, some in worker B, the rest served right here.
Worker side
serveHttp(app, { listen: 0 }) binds an internal 127.0.0.1 port and announces it to the parent — the gateway owns the only public port.
// worker entry — listen on an internal port and announce it
import { serveHttp } from '@atolljs/node/http';
serveHttp(createApp(), { listen: 0 }); // → parent gets {type:'HTTP_PORT',port}Main thread
// :3204 — gateway: split one listener by route prefix (any Node)
import { routeHttpGateway } from '@atolljs/node/http';
routeHttpGateway({
pool,
port: 3204,
routes: [
{ prefix: '/api/a/', to: '/api/', worker: 0 }, // always worker A
{ prefix: '/api/b/', to: '/api/', worker: 1 }, // always worker B
],
handler: mainHandler, // everything else stays on the API thread
});worker:is a slot index into the livepool.workerssnapshot (or a selector fn) — a respawned worker re-announces its port and takes over its routes automatically.to:rewrites the prefix —/api/a/incidents/queryreaches the worker as/api/incidents/query; the prefix only names the owner.- A route whose worker hasn't announced yet gets a 503.
- WebSocket upgrades match the same prefix table and tunnel end-to-end — see WebSockets.
Embedding in a host framework
When the main thread's HTTP stack belongs to Express/Nest/etc., mount just the proxy piece instead of running the standalone gateway: workerHttpPorts tracks announcements (respawns re-announce on the next refresh(); HTTP_PORT_QUERY covers announcements that raced the attach) and proxyToWorker resolves the target worker per request.
// Embedding into a host framework — mountable pieces instead of
// the standalone gateway:
import { workerHttpPorts, proxyToWorker } from '@atolljs/node/http';
const tracker = workerHttpPorts(pool); // HTTP_PORT handshake tracker
app.use('/api/housed', proxyToWorker({
pool, tracker, to: '/api/housed', // restore the stripped mount
worker: (w) => w[i++ % w.length], // or a slot index to pin
}));The NestJS version of this pattern — a whole Nest application housed inside workers — is Backend → NestJS → Housed APIs.
Two pools, one shared buffer
Worker-housed routes get their own pool and worker entry — but a pool's sharedMemory config allocates a new buffer. To let a message-only pool read another pool's contract memory, hand each spawned worker the existing buffer:
// Two pools, one buffer — a message-only pool reading another
// pool's contract memory (the housed-API pattern):
import { withSharedBuffer } from '@atolljs/node';
const housed = createNodePool({
// NO sharedMemory here — a second pool would allocate a second buffer.
worker: withSharedBuffer(
() => new Worker(new URL('../dist/housed.worker.js', import.meta.url)),
() => pool.sharedBuffer, // resolved per spawn — respawns included
),
poolSize: 2,
});
// housed.worker.ts — receive + bind before serving:
import { bindSharedBuffer } from '@atolljs/node';
await bindSharedBuffer(); // same SharedArrayBuffer as the incidents poolLive example
examples/http-offload runs the gateway on port 3204. Start it via npm run serve:all, then hit:
http://localhost:3204/api/whoami— answered by the API threadhttp://localhost:3204/api/a/whoami— always worker Ahttp://localhost:3204/api/b/incidents/stats— worker-computed aggregates inside worker Bhttp://localhost:3204/api/incidents/42— direct shared-memory read, zero dispatch
For zero main-thread parsing on Node ≥ 26, see Clustering.