Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions apps/docs/app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -55,7 +55,7 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
),
}}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-center justify-between gap-4">
<DocsTitle>{page.data.title}</DocsTitle>
<PageActions
markdownUrl={`${page.url}.mdx`}
Expand Down Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/docs/layout.tsx
Original file line number Diff line number Diff line change
@@ -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">) {
Expand Down
5 changes: 3 additions & 2 deletions apps/docs/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -18,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.",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/components/ai/page-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export function PageActions({
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 cursor-pointer rounded-l-[5px] hover:bg-fd-accent hover:text-fd-accent-foreground"
>
{checked ? <Check /> : <Copy />}
{checked ? "Copied!" : "Copy page"}
<span className="hidden lg:inline">{checked ? "Copied!" : "Copy page"}</span>
</button>
<span className="w-px self-stretch bg-fd-border" />
<PopoverTrigger className="inline-flex items-center self-stretch px-1.5 cursor-pointer rounded-r-[5px] hover:bg-fd-accent hover:text-fd-accent-foreground">
Expand Down
204 changes: 204 additions & 0 deletions apps/docs/content/docs/get-started.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Steps>
<Step>

### 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
```

</Step>

<Step>
### 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 {};
},
});

Bun.serve({ fetch: zen.fetch, port: 8000 });
```

```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);
```

</Step>

<Step>

### 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 };
},
});
```

</Step>

<Step>
### 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/<postId>",

async ({ p, auth }) => {
const post = await db.getPostByUser(auth.currentUser.id, p.postId);

if (!post) {
abort(404);
}

return { id: post.id, title: post.title };
}
);
```

</Step>

<Step>

### 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";
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 };
}
);
```

</Step>

<Step>

### Learn more

Get started with [CORS](/docs/cors), [error handling](/docs/error-handling)...

</Step>

</Steps>
38 changes: 11 additions & 27 deletions apps/docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
@@ -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.
---

Expand All @@ -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

Expand Down Expand Up @@ -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`.
1 change: 1 addition & 0 deletions apps/docs/content/docs/meta.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"pages": [
"index",
"get-started",
"---Concepts---",
"routes",
"authorization",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/lib/layout.shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function baseOptions(): BaseLayoutProps {
>
<path d="M36.378.084C41.49.47 47.388 6.63 47.388 12.17c0 2.014-2.435 5.283-9.83 6.546-5.899 1.007-11.797-.86-14.943-2.518-6.685-3.525-4.03-8.718-1.573-11.078C24.974 1.343 29.693-.42 36.378.084zM58.243 40.145C57.03 46.142 47.64 48 31.733 48 20.07 48 7.876 45.382 8.406 37.527 8.935 29.673 18.48 24 33.325 24c14.844 0 26.509 8.29 24.918 16.145zM66.941 66.13c-1.104-6.993-11.206-13.186-32.497-13.693C23.238 51.69-5.189 59.15.826 71.202 7.39 84.35 30.89 82.867 46.77 82.867c6.817 0 21.852-6.086 20.171-16.737z" />
</svg>
<span>Zen Router</span>
<span className="text-sm">Zen Router</span>
</>
),
},
Expand Down
4 changes: 0 additions & 4 deletions apps/docs/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down