Skip to content

Why Nwire

You're building a backend. You need the boring, essential things on day one: auth, a database, file storage, email, background jobs, and the discipline not to leak a stack trace to a customer. You wire those up.

Then a second thing needs the same logic. The job that runs nightly needs to do what the HTTP endpoint does. An internal tool needs to call it. A queue worker needs to retry it. So you copy the logic, or you refactor it out of the controller, or you build an internal RPC layer. Either way, the operation you already wrote is now tangled with the one way you first happened to call it.

Nwire's bet is that the operation and the transport are different things, and the framework should keep them apart for you.

The shape, and why

A handler is a plain typed function. It takes validated input and a context (logger, the current user, a way to resolve dependencies). It does not know whether it was called by an HTTP route, a queue message, a cron tick, or an MCP tool. You wire it to one or more of those:

ts
app.wire(post("/orders/:id/charge"), charge);
app.wire(queue("orders.charge"), charge);   // same function, another door

That single seam is the whole idea. Adding a transport is a line of wiring. There is no controller to rewrite, no logic to copy, no RPC layer to maintain. When you later split a monolith into services, the handler still doesn't change — only where the endpoint sends events does.

Batteries live behind contracts. Auth, storage, mail, queues, RBAC, schedules — each is a small interface with an in-memory default and swappable adapters. You code against @nwire/storage; you run @nwire/storage-s3 in production and the in-memory one in tests. The same holds for auth (Logto, better-auth), mail, and queues (BullMQ). You are never locked to one vendor, and your code never mentions which one.

Production and observability are defaults, not a checklist. A fresh app drains gracefully on SIGTERM, answers Kubernetes liveness and readiness probes, and redacts raw error messages from client responses unless you opt in. Every dispatch carries tenant, user, correlation, and causation, so Studio can show you a real trace of what happened rather than a wall of disconnected spans.

Decoupling is built in. Emit an event in one place and react to it in another, the way you'd reach for events and listeners in Laravel. The emitter doesn't know who listens; the listener lives in its own file. This is core, not an add-on, and it keeps features from growing into each other.

It grows with you, and never ahead of you. Most of an app is routes, handlers, events, and the batteries above. When the logic gets deeper, the primitives are already in the box: a multi-step process that spans time (a workflow), something that holds state and enforces a rule (a seat that can't be double-booked), a read model you query. You reach for them when you feel the need, not because a tutorial told you to start there.

The mental model

Five ideas carry the whole framework. Learn them in this order and the rest of the docs read as consequences.

  1. The operation is not the transport. A handler is a plain typed function. You wire it to an HTTP route, a queue, a cron tick, an MCP tool — the handler never learns which one called it. Adding a caller is a line of wiring, not a second copy.
  2. An app is a bounded context; its registry is its primitives. You write define* exports — actions, queries, actors, workflows — and they're registered for you through the app's registry. You never hand-list them, and no primitive is ever passed to a plugin.
  3. Config is the control panel, bound at boot. One typed environment contract, one slice per concern, secrets kept in the environment and masked in Studio. Handlers read ctx.config; nothing is read behind your back.
  4. Batteries you grow into. Auth, database, storage, email, queues ship zero-infra and config-driven. Point a driver at a real backend without touching a handler — the contract is the seam.
  5. Forge is opt-in depth. When the logic gets real — state that enforces a rule, a read model you query, a process that spans days — reach for events, actors, projections, and workflows. Until then they stay out of your way.

Why not just…

…Nest? Nest owns structured TypeScript backends and has a deep ecosystem. But a Nest provider is bound to its transport by construction — an HTTP controller is an HTTP controller. Nwire's handler is transport-agnostic by construction. Choose Nest for a conventional HTTP/GraphQL service with a large module ecosystem; choose Nwire when one operation must run, unchanged, under several transports.

…Fastify or Hono? They own fast-and-simple HTTP, and they're excellent at it. They don't model the operation above the route, and auth, storage, observability, and background work are yours to assemble and keep consistent. Choose them for a lean HTTP service; choose Nwire when you want the operation, the batteries, and the trace view without assembling them yourself.

…Encore or Effect? These are the closest on typed, event-driven turf. Nwire's distinctive bet is narrower and more concrete: the endpoint/handler multi-transport seam, batteries behind contracts, and a live domain trace. The heavier domain modeling is available but never the price of entry.

Problems you won't have to solve

Shipping a backend that survives production is a long list of small disciplines most frameworks leave to you. You usually learn them the hard way, one incident at a time. Nwire ships them on by default, so you inherit the experience without the scars:

  • Deploys drop requests. When your orchestrator sends SIGTERM, in-flight requests have to finish and new ones have to stop arriving, or every rollout 500s a few users. Nwire drains gracefully out of the box.
  • Errors leak secrets. A thrown database error can carry a connection string, password and all, straight into the client response. Nwire redacts non-typed errors by default and logs the real one server-side; you opt into verbose messages in dev.
  • Health checks kill you mid-warmup. Kubernetes needs liveness and readiness on a separate path, or it restarts a pod that was still booting. Nwire answers both probes from line one.
  • Concurrency oversells. Two requests booking the last seat both succeed unless the write is serialized per key. Nwire's stores do that by default, so the invariant holds under load.
  • The second consumer forces a rewrite. The day a queue or a cron needs the logic your HTTP route already has, most stacks make you refactor. Nwire's handler is transport-agnostic, so there's nothing to move.

None of these are switches you flip. They're the defaults — you inherit them on line one.

Get started · Hello world and the multi-transport demo

MIT licensed.