endpoint() + lifecycle
@nwire/endpoint is the process-lifecycle layer: graceful shutdown, SIGTERM drain via http-terminator, K8s liveness/readiness probes. The entry-verb shape is endpoint(...).use(adopter).mount(app).run().
Signature
import { endpoint } from "@nwire/endpoint";
function endpoint(name: string, options?: EndpointConfig): EndpointBuilder;
interface EndpointConfig {
readonly banner?: boolean;
readonly probes?: {
readonly enabled?: boolean;
readonly port?: number; // default 9400
};
readonly shutdown?: {
readonly drainTimeout?: number; // default 30_000 (ms)
readonly hardTimeout?: number; // default 5_000 (ms)
readonly drainDelay?: number; // wait between /ready→503 and drain
readonly onShutdown?: () => Promise<void>;
};
readonly exitOnShutdown?: boolean; // tests pass false
}
interface EndpointBuilder {
use(adopter: Adapter): EndpointBuilder; // install a transport adopter
mount(app: App | ComposedApp): EndpointBuilder;
run(): Promise<RunningEndpoint>;
}Minimal
import { createApp } from "@nwire/app";
import { endpoint } from "@nwire/endpoint";
import { get } from "@nwire/wires/http";
import { httpKoa } from "@nwire/koa";
const app = createApp({ appName: "hello" });
app.wire(get("/hello"), async () => ({ hello: "world" }));
await endpoint("hello", { probes: { enabled: true } })
.use(httpKoa({ port: 3000 }))
.mount(app)
.run();With forge
import { createApp, defineRegistry } from "@nwire/app";
import { forgePlugins } from "@nwire/forge";
import { endpoint } from "@nwire/endpoint";
import { httpKoa } from "@nwire/koa";
const app = createApp({
appName: "orders",
registry: defineRegistry({ handlers: [placeOrder] }),
plugins: [...forgePlugins()],
});
await endpoint("orders-api", { probes: { enabled: true } })
.use(httpKoa({ port: 3000 }))
.mount(app)
.run();mount(app) plumbs the App's container into the adopter dispatch so every wire's handler resolves bindings from the App that owns the wire. appCompose(a, b) returns a ComposedApp you mount the same way.
Multi-adopter
Stack adopters on the same endpoint:
import { httpKoa } from "@nwire/koa";
import { queueInMemory } from "@nwire/queue";
import { mcpAdapter } from "@nwire/mcp";
await endpoint("svc", { probes: { enabled: true } })
.use(httpKoa({ port: 3000 }))
.use(queueInMemory())
.use(mcpAdapter())
.mount(app)
.run();Each adopter consumes wires whose binding.$adapter matches its kind.
Boot output
[http-koa] listening on 0.0.0.0:3000
[lifecycle] /ready → 200; servingK8s probes
endpoint({ probes: { enabled: true, port: 9400 } }) opens a separate operational HTTP server on port 9400 with:
GET /live— liveness. 200 once the process is up.GET /ready— readiness. 200 once alladdCheck()contributors pass.
livenessProbe:
httpGet: { path: /live, port: 9400 }
initialDelaySeconds: 10
readinessProbe:
httpGet: { path: /ready, port: 9400 }
initialDelaySeconds: 3
periodSeconds: 5addCheck
Adopters and plugins register readiness contributors via the AdapterBootContext:
// inside an Adapter's boot:
ctx.addCheck({
name: "redis",
check: async () => { await redis.ping(); },
timeout: 2_000,
});Each check's check() returns void on success or throws on failure. The aggregate /ready returns 200 only when every check resolves.
Shutdown sequence
running.shutdown(reason)or SIGTERM/SIGINT received/readyflips to 503drainDelaywait (gives the LB time to redirect)- Each adopter's
shutdown()runs (HTTP drains in-flight, queue stops pulling new jobs, etc.) - App plugins'
dispose()fires in reverse boot order onShutdown()hook (if configured)- Probes server closes
- Process exits (unless
exitOnShutdown: false)
drainTimeout bounds step 4; hardTimeout is the SIGKILL backstop.
Test seam
const running = await endpoint("test", {
exitOnShutdown: false,
banner: false,
probes: { enabled: false },
})
.use(httpKoa({ port: 0 }))
.mount(app)
.run();
// ... test assertions ...
await running.shutdown("test");exitOnShutdown: false keeps the test process alive after the shutdown sequence completes.
See also
@nwire/endpointREADME- HTTP routes guide
- Multi-transport guide — adopters and bus topology