Skip to content

Wires

A wire binds one handler to one way of being called. wire(binding, handler) is the join: the binding says how the logic is reached (an HTTP route, a queue topic, a cron schedule, an MCP tool); the handler is the logic itself. The two stay separate, and the wire is the single line that connects them.

ts
import { createApp } from "@nwire/app";
import { post } from "@nwire/wires/http";
import { z } from "zod";

const app = createApp({ appName: "orders" });

app.wire(
  post("/orders", { body: z.object({ sku: z.string() }) }),
  async (input) => ({ ordered: input.sku }),
);

Bindings come from @nwire/wires

A binding is a small typed descriptor. Each transport contributes its own factory, imported from a subpath so you only pull in what you use:

ImportFactoriesBinds to
@nwire/wires/httpget post put patch delan HTTP route
@nwire/wires/queuequeuea queue topic / worker
@nwire/wires/croncrona schedule
@nwire/wires/mcptoolan MCP tool

The HTTP factories take a path and optional Zod schemas for params, query, and body; the schema is what validates input before the handler runs and what feeds the generated OpenAPI spec. A binding carries no logic — it's just the address.

The same handler, more than one wire

Because a binding is separate from the handler, wiring the same operation to a second transport is another line, never a rewrite:

ts
app.wire(post("/orders/:id/charge", { params: z.object({ id: z.string() }) }), charge);
app.wire(queue("orders.charge", { input: z.object({ id: z.string() }) }), charge);

POST /orders/o-1/charge and an enqueue to orders.charge now run the identical charge handler. This is the property nothing else in the stack gives you for free — see the handler for why it holds.

The verbs that wire

In flux. These verbs are split across two surfaces — the app's wire collection and the transport interface builder — and which verb lives where is being settled alongside the discovery work. The verbs below are real; the surface they attach to may shift. Revisit once discovery lands.

Wiring lives on the app and on the endpoint's interface. The set is small and fixed — there's no transport-specific vocabulary to learn:

  • wire(binding, handler) — bind a handler to a transport binding.
  • from(source) — tag a wire's source label, so routes can be grouped by the feature they belong to (the trace viewer uses this).
  • provide(builder) — accumulate per-request context available to the handler's ctx.
  • merge(other) — append another interface's wires (how composed apps pool their routes).

Reacting to events is deliberately not in this list — that's when in a listener file, not a wire. A wire is a way to be called; a listener is a way to react. Keeping them distinct is what keeps the surface honest.

See also

MIT licensed.