diff --git a/apps/docs/app/docs/[[...slug]]/page.tsx b/apps/docs/app/docs/[[...slug]]/page.tsx index 00469f7..f637faa 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"; @@ -55,7 +55,7 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) { ), }} > -
+
{page.data.title} ) { diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index fa14649..5ba6c56 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"], @@ -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.", diff --git a/apps/docs/components/ai/page-actions.tsx b/apps/docs/components/ai/page-actions.tsx index c69e23e..0e38b55 100644 --- a/apps/docs/components/ai/page-actions.tsx +++ b/apps/docs/components/ai/page-actions.tsx @@ -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 ? : } - {checked ? "Copied!" : "Copy page"} + {checked ? "Copied!" : "Copy page"} diff --git a/apps/docs/content/docs/get-started.mdx b/apps/docs/content/docs/get-started.mdx new file mode 100644 index 0000000..aac5318 --- /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 {}; + }, +}); + +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); +``` + + + + + +### 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.getPostByUser(auth.currentUser.id, p.postId); + + if (!post) { + abort(404); + } + + return { id: post.id, title: post.title }; + } +); +``` + + + + + +### 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 }; + } +); +``` + + + + + +### 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 ), }, 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));