Type safety

Types flow from route definitions to the client with no generation step.

Chaining is required

Type information lives in the type of the router, accumulated one route at a time. Keep the returned value:

// ✅ Types accumulate
const server = new HttpServer()
  .get("/a", (_req, res) => res.json({ a: 1 }))
  .get("/b", (_req, res) => res.json({ b: 2 }));

// ❌ Runtime works, but typeof server knows nothing
const server = new HttpServer();
server.get("/a", (_req, res) => res.json({ a: 1 }));

Route params from the URL

Parameter names are read from the path string - no schema needed for basic strings:

.get("/users/:userId/posts/:postId", (req, res) => {
  req.params.userId; // string
  req.params.postId; // string
  return res.json({ ok: true });
})

Schemas narrow the request

When a schema is provided it replaces the default type for that part of the request:

PartWithout schemaWith schema
paramsinferred from URLschema type
queryRecord<string, string | undefined>schema type
bodyunknownschema type
datamiddleware context-

Response unions

A handler may return several shapes. The client receives their union. Use as const on discriminants. Branches that return nothing contribute nothing (void is dropped).

.get("/users/:id", (req, res) => {
  const user = findUser(req.params.id);
  if (!user) return res.json({ found: false as const });
  return res.json({ found: true as const, name: user.name });
})

const result = await client.get("/users/:id", { params: { id: "42" } });
//    ^ { found: false } | { found: true; name: string }

Middleware context

Each middleware declares what it adds to req.data and what it needs. Wrong order fails at compile time. See the middleware guide.