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:
| Part | Without schema | With schema |
|---|---|---|
| params | inferred from URL | schema type |
| query | Record<string, string | undefined> | schema type |
| body | unknown | schema type |
| data | middleware 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.