Routing

Express-shaped methods, nested routers, and consistent URL matching.

HTTP methods

get, post, put, patch, delete share the signature (url, handler, middlewares?, schemas?).

const server = new HttpServer()
  .get("/users", (_req, res) => res.json({ users: [] }))
  .post("/users", (_req, res) => res.status(201).json({ created: true }))
  .put("/users/:id", (req, res) => res.json({ updated: true, id: req.params.id }))
  .patch("/users/:id", (req, res) => res.json({ patched: true, id: req.params.id }))
  .delete("/users/:id", (_req, res) => res.status(204).send(null));

Query parameters

Parsed automatically. Without a schema, values are string | undefined. With a schema, they are validated and typed - see Validation.

Modular routers

Router<TData> groups routes and declares the req.data context it expects from the parent. Mount with a prefix; prefixes combine at the type level too.

export const usersRouter = new Router<{ user: User }>()
  .get("/users", (_req, res) => res.json({ users: [] }))
  .get("/users/:id", (req, res) =>
    res.json({ id: req.params.id, by: req.data.user.id }));

const server = new HttpServer()
  .use(auth)
  .use("/api", usersRouter);

Mounting is checked at compile time against new Router<TData>(). Middlewares the child registers with its own .use() do not become parent requirements.

URL normalization

Consecutive slashes collapse; trailing slashes are removed (except /). /users, /users/, and //users match the same route.