Skip to content

The endpoint

An endpoint is the running process. It's where your app meets the outside world: it opens the ports, mounts the transports, and owns the lifecycle — boot, serve, drain, stop. A handler is logic; an endpoint is the thing that's actually running when you node dist/main.js.

ts
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 () => ({ message: "hello world" }));

await endpoint("hello", { port: 3000 }).use(httpKoa()).mount(app).run();

That one chain is the whole shape: name the process, use the transports that should serve it, mount the app whose handlers they call, and run.

What you get for free

run() isn't just "listen on a port." An endpoint is production-shaped on the first line, before you configure anything:

  • Graceful drain. On SIGTERM (the signal Kubernetes, Docker, and most process managers send to stop you) the endpoint stops accepting new work, lets in-flight requests finish, disposes plugins in reverse order, then exits. No half-finished requests cut off mid-flight.
  • Health probes. HTTP transports expose liveness and readiness endpoints, so an orchestrator can tell "the process is up" from "the process is ready to serve" — and won't route traffic to a pod that's still booting or already draining.
  • Safe errors. A thrown Error that isn't a typed defineError is redacted to an opaque internal_error before it leaves the process — a driver message or stack trace can't escape to a caller by accident.

These aren't a "production hardening" chapter you graduate to. They're the default the first run() already gives you.

Mounting transports

The verbs on an endpoint are a fixed, small set — .use a transport, .mount an app, .run the process. You don't learn a new surface per transport; you use a different adapter:

ts
await endpoint("orders", { port: 3000 })
  .use(httpKoa())          // serve wired HTTP routes
  .use(queueInMemory())    // serve wired queue topics
  .use(mcpAdapter())       // expose wired actions as MCP tools
  .mount(app)
  .run();

Each adapter reads the app's wires and serves the ones it understands: the HTTP adapter serves get/post/… bindings, the queue adapter serves queue(...) bindings, and so on. The same handler can be wired to several of them — that's the seam the whole framework is built around.

Endpoint vs app

A clean split, worth holding onto:

  • The app is your domain made runnable — handlers, plugins, container, events. It knows nothing about ports or processes.
  • The endpoint is the deployment — which transports are mounted, which port, how the process drains. It knows nothing about your domain.

That's why the same app runs as a single HTTP service, a queue worker, or both at once with no change to a handler: you're changing the endpoint, not the app. It's also how a monolith splits into services later — you compose the same apps under different endpoints (see topology).

See also

MIT licensed.