From f92191532f7d8fb7bf56a94c490eb18fe6a3aaba Mon Sep 17 00:00:00 2001 From: Chris Nicholas Date: Fri, 27 Feb 2026 23:32:03 +0000 Subject: [PATCH 1/5] feat: New layout and get started --- apps/docs/app/docs/[[...slug]]/page.tsx | 2 +- apps/docs/app/docs/layout.tsx | 2 +- apps/docs/app/layout.tsx | 1 - apps/docs/content/docs/get-started.mdx | 204 ++++++++++++++++++++++++ apps/docs/content/docs/index.mdx | 38 ++--- apps/docs/content/docs/meta.json | 1 + apps/docs/lib/layout.shared.tsx | 2 +- 7 files changed, 219 insertions(+), 31 deletions(-) create mode 100644 apps/docs/content/docs/get-started.mdx diff --git a/apps/docs/app/docs/[[...slug]]/page.tsx b/apps/docs/app/docs/[[...slug]]/page.tsx index 00469f7..f250c8d 100644 --- a/apps/docs/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/app/docs/[[...slug]]/page.tsx @@ -4,7 +4,7 @@ import { DocsDescription, DocsPage, DocsTitle, -} from "fumadocs-ui/layouts/docs/page"; +} from "fumadocs-ui/layouts/notebook/page"; import { notFound } from "next/navigation"; import { getMDXComponents } from "@/mdx-components"; import type { Metadata } from "next"; diff --git a/apps/docs/app/docs/layout.tsx b/apps/docs/app/docs/layout.tsx index 1281dfd..3f4a326 100644 --- a/apps/docs/app/docs/layout.tsx +++ b/apps/docs/app/docs/layout.tsx @@ -1,5 +1,5 @@ import { source } from "@/lib/source"; -import { DocsLayout } from "fumadocs-ui/layouts/docs"; +import { DocsLayout } from "fumadocs-ui/layouts/notebook"; import { baseOptions } from "@/lib/layout.shared"; export default function Layout({ children }: LayoutProps<"/docs">) { diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index fa14649..41f9ba9 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -4,7 +4,6 @@ import { Inter, JetBrains_Mono } from "next/font/google"; import { Metadata } from "next"; import { TITLE_DESCRIPTION } from "./docs/[[...slug]]/page"; import { getPageImage, source } from "@/lib/source"; -import { notFound } from "next/navigation"; const inter = Inter({ subsets: ["latin"], diff --git a/apps/docs/content/docs/get-started.mdx b/apps/docs/content/docs/get-started.mdx new file mode 100644 index 0000000..26b84d1 --- /dev/null +++ b/apps/docs/content/docs/get-started.mdx @@ -0,0 +1,204 @@ +--- +title: Get started +description: Install Zen Router with npm, pnpm, bun, or yarn. +--- + +import { Step, Steps } from "fumadocs-ui/components/steps"; + +Zen Router is an opinionated API router built by +[Liveblocks](https://liveblocks.io), where it has been powering billions of +requests per month. It’s designed for Cloudflare Workers, Bun, Node.js, and +other modern JavaScript runtimes. + + + + +### Install Zen Router + +Install the package with your package manager. + +```bash tab="npm" +npm install @liveblocks/zenrouter +``` + +```bash tab="pnpm" +pnpm add @liveblocks/zenrouter +``` + +```bash tab="bun" +bun add @liveblocks/zenrouter +``` + +```bash tab="yarn" +yarn add @liveblocks/zenrouter +``` + + + + +### Create a router + +Create a router instance in your project. + +```ts lineNumbers tab="Cloudflare Workers" +import { ZenRouter } from "@liveblocks/zenrouter"; + +const zen = new ZenRouter({ + authorize: async ({ req }) => { + // ... + return {}; + }, +}); + +export default zen; +``` + +```ts lineNumbers tab="Bun" +import { ZenRouter } from "@liveblocks/zenrouter"; + +const zen = new ZenRouter({ + authorize: async ({ req }) => { + // ... + return {}; + }, +}); + +export default zen; +``` + +```ts lineNumbers tab="Node.js" +import { createServerAdapter } from "@whatwg-node/server"; +import { createServer } from "http"; +import { ZenRouter } from "@liveblocks/zenrouter"; + +const zen = new ZenRouter({ + authorize: async ({ req }) => { + // ... + return {}; + }, +}); + +const zenServer = createServerAdapter(zen.fetch); +const httpServer = createServer(zenServer); + +httpServer.listen(3001); +``` + + + + + +### Authorize requests + +Set up an `authorize` function to [authenticate the request](/docs/authorization). + +```ts lineNumbers +const zen = new ZenRouter({ + authorize: async ({ req }) => { + const token = req.headers.get("Authorization"); + const currentUser = await db.getUserByToken(token); + + if (!currentUser) { + return false; + } + + return { currentUser }; + }, +}); +``` + + + + +### Define a GET route + +Use `zen.route` to [define a route](/docs/routes). + +```ts lineNumbers tab="Cloudflare Workers" +import { abort } from "@liveblocks/zenrouter"; + +// Use params in your route, e.g. `postId` +zen.route( + "GET /api/posts/", + + async ({ p, auth }) => { + const post = await db.getPost(p.postId); + + if (!post) { + abort(404); + } + + return { id: post.id, title: post.title }; + } +); +``` + + + + + +### Define a POST route + +... + +```ts lineNumbers tab="Cloudflare Workers" tab="zod" +import { abort } from "@liveblocks/zenrouter"; +import { z } from "zod"; + +// Validate the request body with zod +zen.route( + "POST /api/posts", + + z.object({ title: z.string() }), + + async ({ auth, body }) => { + const post = await db.createPost({ + title: body.title, + authorId: auth.currentUser.id, + }); + + if (!post.success) { + abort(500); + } + + return { id: post.id, title: post.title }; + } +); +``` + +```ts lineNumbers tab="Cloudflare Workers" tab="decoders" +import { abort } from "@liveblocks/zenrouter"; +import { object, string } from "decoders"; + +// Validate the request body with decoders +zen.route( + "POST /api/posts", + + object({ title: string }), + + async ({ auth, body }) => { + const post = await db.createPost({ + title: body.title, + authorId: auth.currentUser.id, + }); + + if (!post.success) { + abort(500); + } + + return { id: post.id, title: post.title }; + } +); +``` + + + + + +### Learn more + +Get started with [CORS](/docs/cors), [error handling](/docs/error-handling)... + + + + diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx index dce53a1..f8788d3 100644 --- a/apps/docs/content/docs/index.mdx +++ b/apps/docs/content/docs/index.mdx @@ -1,5 +1,5 @@ --- -title: Zen Router +title: Overview description: An opinionated HTTP router with typed path params, built-in body validation, and a clean auth model. --- @@ -8,21 +8,17 @@ Zen Router is an opinionated API router built by requests per month. It’s designed for Cloudflare Workers, Bun, Node.js, and other modern JavaScript runtimes. -```bash tab="npm" -npm install @liveblocks/zenrouter -``` - -```bash tab="pnpm" -pnpm add @liveblocks/zenrouter -``` - -```bash tab="bun" -bun add @liveblocks/zenrouter -``` +## Features -```bash tab="yarn" -yarn add @liveblocks/zenrouter -``` +- [Type-safe everywhere](/docs/routes). Your handler receives `{ p, q, body }`, all fully typed. +- [Body validation](/docs/routes#body-validation). With any [Standard Schema](https://standardschema.dev/) compatible library. +- [Authorization](/docs/authorization). Mandatory by design, opt-out for public routes. +- [Composing routers](/docs/composing-routers). Isolate routers by auth strategy with `ZenRelay`. +- [CORS](/docs/cors). Built-in, not bolted on. +- [Error handling](/docs/error-handling). `abort(404)` from any handler, customizable error shapes. +- [Response helpers](/docs/response-helpers). `json()`, `html()`, `textStream()`, and more. +- [OpenTelemetry](/docs/opentelemetry). Automatic span attributes for matched routes. +- [Runs anywhere](/docs/supported-runtimes). Cloudflare Workers, Bun, Node.js, and any runtime with Web `Request`/`Response`. ## Quick example @@ -283,15 +279,3 @@ zen.route( export default zen; ``` - -## Features - -- [Type-safe everywhere](/docs/routes). Your handler receives `{ p, q, body }`, all fully typed. -- [Body validation](/docs/routes#body-validation). With any [Standard Schema](https://standardschema.dev/) compatible library. -- [Authorization](/docs/authorization). Mandatory by design, opt-out for public routes. -- [Composing routers](/docs/composing-routers). Isolate routers by auth strategy with `ZenRelay`. -- [CORS](/docs/cors). Built-in, not bolted on. -- [Error handling](/docs/error-handling). `abort(404)` from any handler, customizable error shapes. -- [Response helpers](/docs/response-helpers). `json()`, `html()`, `textStream()`, and more. -- [OpenTelemetry](/docs/opentelemetry). Automatic span attributes for matched routes. -- [Runs anywhere](/docs/supported-runtimes). Cloudflare Workers, Bun, Node.js, and any runtime with Web `Request`/`Response`. diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json index 9bbe09b..83b2d29 100644 --- a/apps/docs/content/docs/meta.json +++ b/apps/docs/content/docs/meta.json @@ -1,6 +1,7 @@ { "pages": [ "index", + "get-started", "---Concepts---", "routes", "authorization", diff --git a/apps/docs/lib/layout.shared.tsx b/apps/docs/lib/layout.shared.tsx index 9bfcaba..468aa8a 100644 --- a/apps/docs/lib/layout.shared.tsx +++ b/apps/docs/lib/layout.shared.tsx @@ -21,7 +21,7 @@ export function baseOptions(): BaseLayoutProps { > - Zen Router + Zen Router ), }, From af425ff075307a09540f8b9b43e685a776eb9220 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Tue, 10 Mar 2026 09:33:24 +0100 Subject: [PATCH 2/5] Make BASE_URL optional --- apps/docs/app/docs/[[...slug]]/page.tsx | 4 +++- apps/docs/app/layout.tsx | 4 +++- apps/docs/next.config.mjs | 4 ---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/docs/app/docs/[[...slug]]/page.tsx b/apps/docs/app/docs/[[...slug]]/page.tsx index f250c8d..6d6ed65 100644 --- a/apps/docs/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/app/docs/[[...slug]]/page.tsx @@ -91,7 +91,9 @@ export async function generateMetadata( const isDocsHome = page.url === "/docs"; return { - metadataBase: new URL(process.env.BASE_URL!), + metadataBase: new URL( + process.env.BASE_URL ?? "http://localhost:3000" + ), title: isDocsHome ? TITLE_DESCRIPTION : `${page.data.title} | Zen Router`, description: page.data.description, openGraph: { diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 41f9ba9..5ba6c56 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -17,7 +17,9 @@ const jetbrainsMono = JetBrains_Mono({ const page = source.getPage(["/docs"]); export const metadata: Metadata = { - metadataBase: new URL(process.env.BASE_URL!), + metadataBase: new URL( + process.env.BASE_URL ?? "http://localhost:3000" + ), title: TITLE_DESCRIPTION, description: "An opinionated HTTP router with typed path params, built-in body validation, and a clean auth model. Compatible with Cloudflare Workers, Node.js, Bun, Deno, and other modern JavaScript runtimes. Built by Liveblocks.", diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 39f7525..77810b7 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -2,10 +2,6 @@ import { createMDX } from "fumadocs-mdx/next"; import { fileURLToPath } from "url"; import { dirname } from "path"; -if (!process.env.BASE_URL) { - throw new Error("BASE_URL environment variable is not set"); -} - const withMDX = createMDX(); const __dirname = dirname(fileURLToPath(import.meta.url)); From 09a25be5db3ddaba502e4b5a4cd295c4084f6742 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Tue, 10 Mar 2026 09:21:12 +0100 Subject: [PATCH 3/5] Improve mobile layout Hide "Copy page" text on smaller screens, and vertically align title with action button. --- apps/docs/app/docs/[[...slug]]/page.tsx | 2 +- apps/docs/components/ai/page-actions.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/app/docs/[[...slug]]/page.tsx b/apps/docs/app/docs/[[...slug]]/page.tsx index 6d6ed65..f637faa 100644 --- a/apps/docs/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/app/docs/[[...slug]]/page.tsx @@ -55,7 +55,7 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) { ), }} > -
+
{page.data.title} {checked ? : } - {checked ? "Copied!" : "Copy page"} + {checked ? "Copied!" : "Copy page"} From 9b8e771e9407ea3e416b08dd3f5ca2970e9b3474 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Tue, 10 Mar 2026 09:22:21 +0100 Subject: [PATCH 4/5] Update GET route example to use auth.currentUser --- apps/docs/content/docs/get-started.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/docs/get-started.mdx b/apps/docs/content/docs/get-started.mdx index 26b84d1..2a20e60 100644 --- a/apps/docs/content/docs/get-started.mdx +++ b/apps/docs/content/docs/get-started.mdx @@ -122,7 +122,7 @@ zen.route( "GET /api/posts/", async ({ p, auth }) => { - const post = await db.getPost(p.postId); + const post = await db.getPostByUser(auth.currentUser.id, p.postId); if (!post) { abort(404); From cb575487c07566268bd9c0cab716dfc21826ee33 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Tue, 10 Mar 2026 09:30:22 +0100 Subject: [PATCH 5/5] Improve Get Started guide Fixes the Bun example, adds a POST route description --- apps/docs/content/docs/get-started.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/docs/get-started.mdx b/apps/docs/content/docs/get-started.mdx index 2a20e60..aac5318 100644 --- a/apps/docs/content/docs/get-started.mdx +++ b/apps/docs/content/docs/get-started.mdx @@ -63,7 +63,7 @@ const zen = new ZenRouter({ }, }); -export default zen; +Bun.serve({ fetch: zen.fetch, port: 8000 }); ``` ```ts lineNumbers tab="Node.js" @@ -139,7 +139,7 @@ zen.route( ### Define a POST route -... +Now add a second endpoint. Pass a schema to [validate the request body](/docs/routes#body-validation). ```ts lineNumbers tab="Cloudflare Workers" tab="zod" import { abort } from "@liveblocks/zenrouter";