Composing routers
Composing multiple routers with different auth requirements using Zen Relay.
A typical backend serves endpoints to different audiences, each with its own
requirements: your main API might need token auth, admin routes require stricter
access, webhook receivers verify request signatures, and some routes need no auth
at all. Different audiences may also need different error responses — user-facing
APIs benefit from friendly, helpful error messages, while internal endpoints can
be more terse. Zen Router lets you group routes with the same requirements into
separate ZenRouter instances, then bind them together with ZenRelay.
If you only need a single auth strategy, a single ZenRouter is enough. No
ZenRelay needed.
ZenRelay
ZenRelay is nothing but a thin dispatch layer. All it does is look at an
incoming request, select which router handles it based on the given URL prefix,
and pass it to the first matching router. It does no parsing, no validation, no
auth — that’s all handled by the ZenRouter instance it dispatches to.
importfrom "@liveblocks/zenrouter";
importasfrom "./routes/auth";
importasfrom "./routes/api";
importasfrom "./routes/admin";
importasfrom "./routes/webhooks";
const app = new ZenRelay
app.relay"/auth/*", authRoutes);
app.relay"/api/admin/*", adminRoutes);
app.relay"/api/*", apiRoutes);
app.relay"/webhooks/*", webhookRoutes);
export defaultRoutes inside each router are still fully qualified. The relay prefix is only used for dispatch. It does not strip or rewrite the URL. Each router receives the original, unmodified request:
const zen = new ZenRouter
zen.route"GET /api/posts", handler);
zen.route"GET /api/posts/<postId>", handler);
// ^^^^^^^^^^^^^^^^^^^^^^^^
// Always the full path, never relative to the relay prefix
exportNo base prefixes
There is no way to set a "base" prefix on a ZenRouter or ZenRelay that gets
prepended to your route definitions. Every route pattern is always the
complete, greppable URL path. This is intentional.
If you want to find the handler for /api/posts, you grep for /api/posts.
No indirection, no prefix math.
No fall-through
There is no implicit fall-through between routers. This is a core principle.
For example, if a request is made to /api/admin/users/123, only the
/api/admin/* router will ever see it. If that router doesn’t have a matching
route handler, it will return 404. The /api/* router will deliberately
not get a chance to handle it. Each router has its own auth and error
handling behavior, so routers are fully isolated from each other.
Keep it flat
The encouraged pattern is a single ZenRelay layer at the top level,
dispatching to one set of ZenRouter instances. Don’t nest relays or routers.
It only makes the routing harder to follow.