NestJS — housed APIs
A route subtree that lives only inside workers: a dedicated message-only pool boots a real Nest app per worker — decorators, DI, and guards intact — and the main app proxies a URL prefix into it. The whole controller stack executes off the API thread.
The message-only pool
The housed pool carries no sharedMemory of its own — a second pool would allocate a second buffer. withSharedBuffer threads the incidents pool's existing buffer into every spawned worker (respawns included), so housed controllers read the same contract state the API thread owns.
// src/housed/housed-atoll.module.ts — the 'housed' pool is
// MESSAGE-ONLY: a sharedMemory here would allocate a SECOND buffer, so
// withSharedBuffer hands each spawned worker the incidents pool's
// buffer instead. Two pools' workers, one shared buffer.
import { Module } from '@nestjs/common';
import { Worker } from 'node:worker_threads';
import { AtollModule, getAtollPool } from '@atolljs/nestjs';
import { withSharedBuffer } from '@atolljs/node';
@Module({
imports: [
AtollModule.registerPool({
name: 'housed',
worker: withSharedBuffer(
() => new Worker(new URL('./housed.worker.ts', import.meta.url)),
() => getAtollPool('incidents')?.sharedBuffer, // lazy — respawns included
),
poolSize: 2,
}),
],
})
export class HousedAtollModule {}The worker — a whole Nest app
No runAtollWorker here: this is an HTTP worker, not a task worker. It binds the shared buffer, boots HousedApiModule with NestFactory, and hands its HTTP server to serveHttp — which listens on an internal 127.0.0.1 port (announced to the parent for the gateway path) and accepts transferred sockets (for clustering).
// src/housed/housed.worker.ts — an HTTP worker, NOT a task worker
// (no runAtollWorker/bootstrap): bind the shared buffer, then boot a
// whole Nest app that exists ONLY inside workers.
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { bindSharedBuffer } from '@atolljs/node';
import { serveHttp } from '@atolljs/node/http';
import { HousedApiModule } from './housed-api.module';
void (async () => {
await bindSharedBuffer(); // the incidents pool's buffer — same memory
const app = await NestFactory.create(HousedApiModule, { logger: ['warn', 'error'] });
await app.init();
serveHttp(app.getHttpServer(), { listen: 0 }); // internal port, announced to parent
})();Main thread — three entry points to the same routes
// src/main.ts — /api/housed/* proxies into workers; the rest of
// the app keeps its own controllers on the API thread.
import { proxyToWorker, proxyUpgradeToWorker, createHttpCluster,
workerHttpPorts } from '@atolljs/node/http';
const pool = getAtollPool('housed');
const tracker = workerHttpPorts(pool);
let cursor = 0;
app.use(
'/api/housed',
proxyToWorker({
pool,
tracker,
to: '/api/housed', // express stripped the mount — restore it for worker routes
worker: (workers) => workers[cursor++ % workers.length], // round-robin
}),
);
// WebSocket upgrades bypass middleware — attach the upgrade sibling to the
// server itself (full URL, nothing stripped, so no 'to' needed).
app.getHttpServer().on('upgrade',
proxyUpgradeToWorker({ pool, tracker, worker: (w) => w[cursor++ % w.length] }));
// Optional second listener: cluster the housed pool (Node ≥ 26). The main
// thread hands each accepted socket to a housed worker unparsed — zero
// parsing/serialization on the API thread. Per-CONNECTION routing can't
// split a port by path, so it lives on its own port. Below Node 26 the
// call logs a notice and returns null — no capability check needed.
createHttpCluster({ pool, port: port + 1 });proxyToWorker keeps /api/housed/* on the app's own port — one origin for clients, one port to expose — at the cost of a parse + proxy hop on the API thread. createHttpCluster skips even that: its dedicated listener hands each connection to a worker unparsed, but per-connection routing can't share the app port by URL path, so it lives on PORT + 1. See Clustering and WebSockets for the other two entry points.
What executes where
Trace GET :3100/api/housed/incidents/whoami through the proxy path — the stages split across two threads:
- API thread — accepts the connection, Express parses the request,
app.use('/api/housed', ...)matches, andproxyToWorkerpicks a housed worker (round-robincursor) and rewrites the mount-stripped path back to/api/housed/.... - API thread → worker — the request is re-serialized onto that worker's internal
127.0.0.1port (the oneserveHttp({ listen: 0 })announced viaworkerHttpPorts). Method, path, headers, and body all cross — this hop is the proxy's cost. - Housed worker thread — the request lands on the worker's own HTTP server, so a complete Nest pipeline executes off-thread: guards, interceptors, DI resolution, the controller body — which reads the incidents buffer directly (the same SharedArrayBuffer
bindSharedBuffer()attached, so the row read is a memory read, not a message). - Worker → API thread → client — the response streams back over the internal port and the gateway pipes it to the client untouched — status, headers, the stamped
threadIdin the body.
The clustered path on :3101 removes steps 2 and 4's application-level hop: the API thread hands the whole TCP socket to a worker unparsed, and that worker parses HTTP, runs the same Nest pipeline, and answers the client directly. The API thread only ever saw an opaque connection. Same controllers either way — the difference is purely how bytes reach the worker:
| Stage | App routes (/api/*) | Proxied (/api/housed/*) | Clustered (:3101) |
|---|---|---|---|
| Accept + parse | API thread | API thread | API thread accepts, worker parses |
| Nest pipeline (guards, DI, controller) | API thread | housed worker | housed worker |
| Incidents data access | memory read / task dispatch | direct memory read in worker | direct memory read in worker |
| Response path | direct to client | worker → internal port → API thread → client | worker → client |
DI inside housed controllers
Housed controllers inject normally — IncidentsAnalytics's @AtollService methods find an empty pool registry in-worker and run their real bodies, so per-worker state (telemetry, caches) stays genuinely per-worker. The standalone pieces — serveHttp, workerHttpPorts, proxyToWorker, withSharedBuffer, bindSharedBuffer — come from @atolljs/node and work without Nest too.
Live example
examples/nestjs serves the housed API both ways. Start it via npm run serve:all, then:
http://localhost:3100/api/housed/incidents/whoami— proxied on the app port; the response stamps the owning worker's threadIdhttp://localhost:3100/api/housed/incidents/worker-telemetry— per-worker state from the housed Nest app:3101— the clustered listener (Node ≥ 26): same routes, reached without the proxy hop