Skip to content

Route bindings

Verb factories produce RouteBinding values that app.wire(binding, handler) consumes. Each carries the HTTP verb, path, optional schemas (params/query/body), per-route middleware, and OpenAPI metadata.

Factories

ts
import { get, post, put, patch, del } from "@nwire/wires/http";

const listUsers   = get("/users");
const getUser     = get("/users/:id");
const createUser  = post("/users");
const updateUser  = put("/users/:id");
const patchUser   = patch("/users/:id");
const deleteUser  = del("/users/:id");

With schemas

ts
import { z } from "zod";

const getUser = get("/users/:id", {
  params: z.object({ id: z.string() }),
});

const createUser = post("/users", {
  body: z.object({
    email: z.email(),
    name:  z.string().min(1).max(100),
  }),
});

const listUsersFiltered = get("/users", {
  query: z.object({
    role:  z.enum(["admin", "user"]).optional(),
    limit: z.coerce.number().int().min(1).max(100).default(20),
  }),
});

const updateUser = put("/users/:id", {
  params: z.object({ id: z.string() }),
  body:   z.object({ name: z.string().optional() }),
});

The handler's input is the merged + validated result of all three.

Per-route middleware

ts
const purge = post("/admin/purge", {
  middleware: [requireAdmin, logSensitive],
});

Middleware runs in declaration order, before the handler.

OpenAPI metadata

ts
const createUser = post("/users", {
  body: UserInput,
  openapi: {
    operation:   "CreateUser",
    summary:     "Create a user",
    description: "Idempotent — POSTing the same email returns the existing row.",
    tags:        ["users"],
    returns:     [ok200User],
    errors:      [UserNotFound, EmailAlreadyTaken],
  },
});

Surfaced at /openapi.json when httpKoa({ openapi: { auto: true } }) is set; Scalar UI at /docs when docs: true.

Auth policy

The policy field on the underlying action carries an authz tag consumed by @nwire/auth and @nwire/rbac plugins. It's opaque to the framework — the authorizer decides what each tag means. When you wire an action's handler directly, the binding picks up the action's policy automatically.

Status codes

The default 2xx status is inferred from the handler return:

  • plain value (object/array) → 200
  • null / undefined → 204
  • { $kind: "response-instance", status, body } (from defineResource) → status
  • { $status, body } (legacy envelope) → $status

Type inference

ts
const route = post("/users", {
  body: z.object({ email: z.email() }),
});

// route is RouteBinding<{ email: string }>
// app.wire(route, handler) infers handler's input type

See also

MIT licensed.