diff --git a/automd.config.ts b/automd.config.ts index f0aa80ebce..5d44fe0d0c 100644 --- a/automd.config.ts +++ b/automd.config.ts @@ -1,4 +1,190 @@ import type { Config } from "automd"; +import { readdir, stat, readFile } from "node:fs/promises"; +import { join, extname, relative } from "pathe"; + +interface FileEntry { + path: string; + relativePath: string; + content: string; + language: string; +} + +const DEFAULT_IGNORE = [ + "node_modules", + ".git", + ".DS_Store", + ".nuxt", + ".output", + ".nitro", + "dist", + "coverage", + ".cache", + ".turbo", + "pnpm-lock.yaml", + "package-lock.json", + "yarn.lock", +]; + +const EXTENSION_LANGUAGE_MAP: Record = { + ".ts": "ts", + ".tsx": "tsx", + ".js": "js", + ".jsx": "jsx", + ".mjs": "js", + ".cjs": "js", + ".vue": "vue", + ".json": "json", + ".html": "html", + ".css": "css", + ".scss": "scss", + ".md": "md", + ".yaml": "yaml", + ".yml": "yaml", + ".toml": "toml", + ".sh": "bash", + ".bash": "bash", + ".zsh": "bash", +}; + +async function parseGitignore(dir: string): Promise { + try { + const gitignorePath = join(dir, ".gitignore"); + const content = await readFile(gitignorePath, "utf8"); + return content + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")); + } catch { + return []; + } +} + +function shouldIgnore(name: string, ignorePatterns: string[], defaultIgnore: string[]): boolean { + const allPatterns = [...defaultIgnore, ...ignorePatterns]; + for (const pattern of allPatterns) { + const cleanPattern = pattern.replace(/^\//, "").replace(/\/$/, ""); + if (name === cleanPattern) { + return true; + } + if (pattern.startsWith("*") && name.endsWith(pattern.slice(1))) { + return true; + } + if (pattern.endsWith("*") && name.startsWith(pattern.slice(0, -1))) { + return true; + } + } + return false; +} + +function getLanguage(filePath: string): string { + const ext = extname(filePath).toLowerCase(); + return EXTENSION_LANGUAGE_MAP[ext] || "text"; +} + +async function collectFiles( + dir: string, + baseDir: string, + ignorePatterns: string[], + maxDepth: number, + currentDepth: number = 0 +): Promise { + if (maxDepth > 0 && currentDepth >= maxDepth) { + return []; + } + + const entries = await readdir(dir); + const files: FileEntry[] = []; + + for (const entry of entries) { + if (shouldIgnore(entry, ignorePatterns, DEFAULT_IGNORE)) { + continue; + } + + const fullPath = join(dir, entry); + const stats = await stat(fullPath); + + if (stats.isDirectory()) { + const nestedFiles = await collectFiles( + fullPath, + baseDir, + ignorePatterns, + maxDepth, + currentDepth + 1 + ); + files.push(...nestedFiles); + } else { + try { + const content = await readFile(fullPath, "utf8"); + const relativePath = relative(baseDir, fullPath); + files.push({ + path: fullPath, + relativePath, + content: content.trim(), + language: getLanguage(fullPath), + }); + } catch { + // Skip binary or unreadable files + } + } + } + + return files; +} + +function sortFiles(files: FileEntry[]): FileEntry[] { + return files.sort((a, b) => { + const aParts = a.relativePath.split("/"); + const bParts = b.relativePath.split("/"); + + // Sort by depth first (shallower files first) + if (aParts.length !== bParts.length) { + return aParts.length - bParts.length; + } + + // Then alphabetically + return a.relativePath.localeCompare(b.relativePath); + }); +} + +function generateCodeTree( + files: FileEntry[], + options: { defaultValue?: string; expandAll?: boolean } = {} +): string { + const sortedFiles = sortFiles(files); + const codeBlocks: string[] = []; + + for (const file of sortedFiles) { + const lang = file.language; + const filename = file.relativePath; + + // Use 4 backticks for markdown files to avoid conflicts + const fence = lang === "md" ? "````" : "```"; + codeBlocks.push(`${fence}${lang} [${filename}]`); + codeBlocks.push(file.content); + codeBlocks.push(fence); + codeBlocks.push(""); + } + + const attrs: string[] = []; + if (options.defaultValue) { + attrs.push(`defaultValue="${options.defaultValue}"`); + } + if (options.expandAll) { + attrs.push(`expandAll`); + } + const propsStr = attrs.length > 0 ? `{${attrs.join(" ")}}` : ""; + const contents = `::code-tree${propsStr}\n\n${codeBlocks.join("\n").trim()}\n\n::`; + + return contents; +} + +function resolvePath(srcPath: string, options: { url?: string; dir?: string }): string { + if (srcPath.startsWith("/")) { + return srcPath; + } + const base = options.url ? new URL(".", options.url).pathname : options.dir || process.cwd(); + return join(base, srcPath); +} export default { input: ["README.md", "docs/**/*.md"], @@ -22,5 +208,51 @@ export default { }; }, }, + "ui-code-tree": { + name: "ui-code-tree", + async generate({ + args, + config, + url, + }: { + args: Record; + config: { dir?: string }; + url?: string; + }) { + const srcPath = (args.src as string) || "."; + const fullPath = resolvePath(srcPath, { url, dir: config.dir }); + + const stats = await stat(fullPath); + if (!stats.isDirectory()) { + throw new Error(`Path "${srcPath}" is not a directory`); + } + + const userIgnore: string[] = args.ignore + ? String(args.ignore) + .split(",") + .map((s: string) => s.trim()) + : []; + + const gitignorePatterns = await parseGitignore(fullPath); + const ignorePatterns = [...gitignorePatterns, ...userIgnore]; + + const maxDepth = args.maxDepth ? Number(args.maxDepth) : 0; + const defaultValue = (args.defaultValue || args.default) as string | undefined; + const expandAll = args.expandAll !== undefined && args.expandAll !== "false"; + + const files = await collectFiles(fullPath, fullPath, ignorePatterns, maxDepth); + + if (files.length === 0) { + return { + contents: "", + issues: ["No files found in the specified directory"], + }; + } + + const contents = generateCodeTree(files, { defaultValue, expandAll }); + + return { contents }; + }, + }, }, } satisfies Config; diff --git a/docs/.docs/content.config.ts b/docs/.docs/content.config.ts new file mode 100644 index 0000000000..256c9a662b --- /dev/null +++ b/docs/.docs/content.config.ts @@ -0,0 +1,20 @@ +import { defineContentConfig, defineCollection, z } from '@nuxt/content' +import { resolve } from 'pathe' + +export default defineContentConfig({ + collections: { + examples: defineCollection({ + type: 'page', + source: { + cwd: resolve(__dirname, '../../examples'), + include: '**/README.md', + prefix: '/examples', + exclude: ['**/.**/**', '**/node_modules/**', '**/dist/**', '**/.docs/**'], + }, + schema: z.object({ + category: z.string().optional(), + icon: z.string().optional(), + }), + }), + }, +}) diff --git a/docs/.docs/layouts/examples.vue b/docs/.docs/layouts/examples.vue new file mode 100644 index 0000000000..80171008db --- /dev/null +++ b/docs/.docs/layouts/examples.vue @@ -0,0 +1,85 @@ + + + diff --git a/docs/.docs/pages/examples/[...slug].vue b/docs/.docs/pages/examples/[...slug].vue new file mode 100644 index 0000000000..888e77fe77 --- /dev/null +++ b/docs/.docs/pages/examples/[...slug].vue @@ -0,0 +1,127 @@ + + + diff --git a/docs/.docs/pages/examples/index.vue b/docs/.docs/pages/examples/index.vue new file mode 100644 index 0000000000..a4ccd216d6 --- /dev/null +++ b/docs/.docs/pages/examples/index.vue @@ -0,0 +1,96 @@ + + + diff --git a/docs/.docs/server/routes/raw/examples/[...slug].md.get.ts b/docs/.docs/server/routes/raw/examples/[...slug].md.get.ts new file mode 100644 index 0000000000..f18ca5f487 --- /dev/null +++ b/docs/.docs/server/routes/raw/examples/[...slug].md.get.ts @@ -0,0 +1,28 @@ +import { queryCollection } from '@nuxt/content/server' +import { stringify } from 'minimark/stringify' +import { withLeadingSlash } from 'ufo' + +export default eventHandler(async (event) => { + const slug = getRouterParams(event)['slug.md'] + if (!slug?.endsWith('.md')) { + throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true }) + } + + // Convert /raw/examples/hello-world.md -> /examples/hello-world/readme + const exampleName = slug.replace('.md', '') + const path = withLeadingSlash(`examples/${exampleName}/readme`) + + const page = await queryCollection(event, 'examples').path(path).first() + if (!page) { + throw createError({ statusCode: 404, statusMessage: 'Example not found', fatal: true }) + } + + // Add title and description to the top of the page if missing + if (page.body.value[0]?.[0] !== 'h1') { + page.body.value.unshift(['blockquote', {}, page.description]) + page.body.value.unshift(['h1', {}, page.title]) + } + + setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8') + return stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' }) +}) diff --git a/docs/.docs/utils/examples.ts b/docs/.docs/utils/examples.ts new file mode 100644 index 0000000000..eac7925c28 --- /dev/null +++ b/docs/.docs/utils/examples.ts @@ -0,0 +1,19 @@ +// Category order for examples - used in sidebar and examples page +export const categoryOrder = [ + 'features', + 'config', + 'server side rendering', + 'backend frameworks', + 'integrations', + 'vite', +] + +export const categoryIcons: Record = { + vite: 'i-logos-vitejs', + 'backend frameworks': 'i-lucide-puzzle', + features: 'i-lucide-sparkles', + config: 'i-lucide-settings', + integrations: 'i-lucide-plug', + 'server side rendering': 'i-lucide-server', + other: 'i-lucide-folder', +} diff --git a/docs/4.examples/0.index.md b/docs/4.examples/0.index.md new file mode 100644 index 0000000000..3c1b20e9c3 --- /dev/null +++ b/docs/4.examples/0.index.md @@ -0,0 +1,7 @@ +--- +icon: i-lucide-folder-code +--- + +# Examples + +> Explore Nitro examples to learn how to build full-stack applications diff --git a/docs/package.json b/docs/package.json index 44dca6a593..da8c3cf9ce 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,6 +4,10 @@ "dev": "undocs dev", "build": "undocs build" }, + "dependencies": { + "automd": "^0.4.3", + "zod": "^4.3.6" + }, "devDependencies": { "shaders": "^2.2.48", "undocs": "^0.4.16" diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index a69c871bf9..ea6d85a6b7 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -7,13 +7,20 @@ settings: importers: .: + dependencies: + automd: + specifier: ^0.4.3 + version: 0.4.3(magicast@0.5.1) + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: shaders: specifier: ^2.2.48 version: 2.2.48 undocs: specifier: ^0.4.16 - version: 0.4.16(@parcel/watcher@2.5.6)(@tiptap/extensions@3.18.0(@tiptap/core@3.18.0(@tiptap/pm@3.18.0))(@tiptap/pm@3.18.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@vue/compiler-sfc@3.5.27)(@vueuse/core@14.2.0(vue@3.5.27(typescript@5.9.3)))(cac@6.7.14)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.9.2)(lightningcss@1.31.1)(magicast@0.5.1)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(yaml@2.8.2)(yjs@13.6.29)(zod@3.25.76) + version: 0.4.16(@parcel/watcher@2.5.6)(@tiptap/extensions@3.18.0(@tiptap/core@3.18.0(@tiptap/pm@3.18.0))(@tiptap/pm@3.18.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@vue/compiler-sfc@3.5.27)(@vueuse/core@14.2.0(vue@3.5.27(typescript@5.9.3)))(cac@6.7.14)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.9.2)(lightningcss@1.31.1)(magicast@0.5.1)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(yaml@2.8.2)(yjs@13.6.29)(zod@4.3.6) packages: @@ -5627,6 +5634,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -6595,7 +6605,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/ui@4.4.0(@nuxt/content@3.11.0(magicast@0.5.1))(@tiptap/extensions@3.18.0(@tiptap/core@3.18.0(@tiptap/pm@3.18.0))(@tiptap/pm@3.18.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.9.2)(magicast@0.5.1)(tailwindcss@4.1.18)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))(yjs@13.6.29)(zod@3.25.76)': + '@nuxt/ui@4.4.0(@nuxt/content@3.11.0(magicast@0.5.1))(@tiptap/extensions@3.18.0(@tiptap/core@3.18.0(@tiptap/pm@3.18.0))(@tiptap/pm@3.18.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.9.2)(magicast@0.5.1)(tailwindcss@4.1.18)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6)': dependencies: '@floating-ui/dom': 1.7.5 '@iconify/vue': 5.0.0(vue@3.5.27(typescript@5.9.3)) @@ -6666,7 +6676,7 @@ snapshots: optionalDependencies: '@nuxt/content': 3.11.0(magicast@0.5.1) vue-router: 4.6.4(vue@3.5.27(typescript@5.9.3)) - zod: 3.25.76 + zod: 4.3.6 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -11647,12 +11657,12 @@ snapshots: magic-string: 0.30.21 unplugin: 2.3.11 - undocs@0.4.16(@parcel/watcher@2.5.6)(@tiptap/extensions@3.18.0(@tiptap/core@3.18.0(@tiptap/pm@3.18.0))(@tiptap/pm@3.18.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@vue/compiler-sfc@3.5.27)(@vueuse/core@14.2.0(vue@3.5.27(typescript@5.9.3)))(cac@6.7.14)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.9.2)(lightningcss@1.31.1)(magicast@0.5.1)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(yaml@2.8.2)(yjs@13.6.29)(zod@3.25.76): + undocs@0.4.16(@parcel/watcher@2.5.6)(@tiptap/extensions@3.18.0(@tiptap/core@3.18.0(@tiptap/pm@3.18.0))(@tiptap/pm@3.18.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@vue/compiler-sfc@3.5.27)(@vueuse/core@14.2.0(vue@3.5.27(typescript@5.9.3)))(cac@6.7.14)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.9.2)(lightningcss@1.31.1)(magicast@0.5.1)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(yaml@2.8.2)(yjs@13.6.29)(zod@4.3.6): dependencies: '@headlessui/vue': 1.7.23(vue@3.5.27(typescript@5.9.3)) '@nuxt/content': 3.11.0(magicast@0.5.1) '@nuxt/fonts': 0.13.0(db0@0.3.4)(ioredis@5.9.2)(magicast@0.5.1)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) - '@nuxt/ui': 4.4.0(@nuxt/content@3.11.0(magicast@0.5.1))(@tiptap/extensions@3.18.0(@tiptap/core@3.18.0(@tiptap/pm@3.18.0))(@tiptap/pm@3.18.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.9.2)(magicast@0.5.1)(tailwindcss@4.1.18)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))(yjs@13.6.29)(zod@3.25.76) + '@nuxt/ui': 4.4.0(@nuxt/content@3.11.0(magicast@0.5.1))(@tiptap/extensions@3.18.0(@tiptap/core@3.18.0(@tiptap/pm@3.18.0))(@tiptap/pm@3.18.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.9.2)(magicast@0.5.1)(tailwindcss@4.1.18)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6) '@nuxtjs/plausible': 2.0.1(magicast@0.5.1) '@resvg/resvg-wasm': 2.6.2 automd: 0.4.3(magicast@0.5.1) @@ -12244,4 +12254,6 @@ snapshots: zod@3.25.76: {} + zod@4.3.6: {} + zwitch@2.0.4: {} diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml index 04b1292e5d..b8691c8845 100644 --- a/docs/pnpm-workspace.yaml +++ b/docs/pnpm-workspace.yaml @@ -1,8 +1,10 @@ packages: [] + ignoredBuiltDependencies: - - "@parcel/watcher" - - "@tailwindcss/oxide" + - '@parcel/watcher' + - '@tailwindcss/oxide' - esbuild - vue-demi + onlyBuiltDependencies: - better-sqlite3 diff --git a/examples/api-routes/GUIDE.md b/examples/api-routes/GUIDE.md new file mode 100644 index 0000000000..6d686ed93a --- /dev/null +++ b/examples/api-routes/GUIDE.md @@ -0,0 +1,51 @@ +Nitro supports file-based routing in the `api/` or `routes/` directory. Each file becomes an API endpoint based on its path. + +## Basic Route + +Create a file in the `api/` directory to define a route. The file path becomes the URL path: + +```ts [api/hello.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Nitro is amazing!"); +``` + +This creates a `GET /api/hello` endpoint. + +## Dynamic Routes + +Use square brackets `[param]` for dynamic URL segments. Access params via `event.context.params`: + +```ts [api/hello/[name].ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`); +``` + +This creates a `GET /api/hello/:name` endpoint (e.g., `/api/hello/world`). + +## HTTP Methods + +Suffix your file with the HTTP method (`.get.ts`, `.post.ts`, `.put.ts`, `.delete.ts`, etc.): + +### GET Handler + +```ts [api/test.get.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Test get handler"); +``` + +### POST Handler + +```ts [api/test.post.ts] +import { defineHandler } from "h3"; + +export default defineHandler(async (event) => { + const body = await event.req.json(); + return { + message: "Test post handler", + body, + }; +}); +``` diff --git a/examples/api-routes/README.md b/examples/api-routes/README.md new file mode 100644 index 0000000000..48c4cdfc58 --- /dev/null +++ b/examples/api-routes/README.md @@ -0,0 +1,159 @@ +--- +category: features +icon: i-lucide-route +--- + +# API Routes + +> File-based API routing with HTTP method support and dynamic parameters. + + + +::code-tree{defaultValue="api/hello.ts" expandAll} + +```html [index.html] + + + + + + API Routes + + +

API Routes:

+ + + +``` + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: "./", +}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +```ts [api/hello.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Nitro is amazing!"); +``` + +```ts [api/test.get.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Test get handler"); +``` + +```ts [api/test.post.ts] +import { defineHandler } from "h3"; + +export default defineHandler(async (event) => { + const body = await event.req.json(); + return { + message: "Test post handler", + body, + }; +}); +``` + +```ts [api/hello/[name].ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`); +``` + +:: + + + + + +Nitro supports file-based routing in the `api/` or `routes/` directory. Each file becomes an API endpoint based on its path. + +## Basic Route + +Create a file in the `api/` directory to define a route. The file path becomes the URL path: + +```ts [api/hello.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Nitro is amazing!"); +``` + +This creates a `GET /api/hello` endpoint. + +## Dynamic Routes + +Use square brackets `[param]` for dynamic URL segments. Access params via `event.context.params`: + +```ts [api/hello/[name].ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`); +``` + +This creates a `GET /api/hello/:name` endpoint (e.g., `/api/hello/world`). + +## HTTP Methods + +Suffix your file with the HTTP method (`.get.ts`, `.post.ts`, `.put.ts`, `.delete.ts`, etc.): + +### GET Handler + +```ts [api/test.get.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Test get handler"); +``` + +### POST Handler + +```ts [api/test.post.ts] +import { defineHandler } from "h3"; + +export default defineHandler(async (event) => { + const body = await event.req.json(); + return { + message: "Test post handler", + body, + }; +}); +``` + + + +## Learn More + +- [Routing](/docs/routing) diff --git a/examples/auto-imports/GUIDE.md b/examples/auto-imports/GUIDE.md new file mode 100644 index 0000000000..40763797b6 --- /dev/null +++ b/examples/auto-imports/GUIDE.md @@ -0,0 +1,35 @@ +Functions exported from `server/utils/` are automatically available without explicit imports when auto-imports are enabled. Define a utility once and use it anywhere in your server code. + +## Configuration + +Enable auto-imports by setting `imports` in your config: + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: true, + imports: {}, +}); +``` + +## Using Auto Imports + +1. Create a utility file in `server/utils/`: + +```ts [server/utils/hello.ts] +export function makeGreeting(name: string) { + return `Hello, ${name}!`; +} +``` + +2. The function is available without importing it: + +```ts [server.ts] +import { defineHandler } from "nitro/h3"; +import { makeGreeting } from "./server/utils/hello.ts"; + +export default defineHandler(() => `

${makeGreeting("Nitro")}

`); +``` + +With this setup, any function exported from `server/utils/` becomes globally available. Nitro scans the directory and generates the necessary imports automatically. diff --git a/examples/auto-imports/README.md b/examples/auto-imports/README.md new file mode 100644 index 0000000000..ae5ac71951 --- /dev/null +++ b/examples/auto-imports/README.md @@ -0,0 +1,108 @@ +--- +category: config +icon: i-lucide-import +--- + +# Auto Imports + +> Automatic imports for utilities and composables. + + + +::code-tree{defaultValue="nitro.config.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: true, + imports: {}, +}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```ts [server.ts] +import { defineHandler } from "nitro/h3"; +import { makeGreeting } from "./server/utils/hello.ts"; + +export default defineHandler(() => `

${makeGreeting("Nitro")}

`); +``` + +```json [tsconfig.json] +{ + "include": [".nitro/types/nitro-imports.d.ts", "src"] +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +```ts [server/utils/hello.ts] +export function makeGreeting(name: string) { + return `Hello, ${name}!`; +} +``` + +:: + + + + + +Functions exported from `server/utils/` are automatically available without explicit imports when auto-imports are enabled. Define a utility once and use it anywhere in your server code. + +## Configuration + +Enable auto-imports by setting `imports` in your config: + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: true, + imports: {}, +}); +``` + +## Using Auto Imports + +1. Create a utility file in `server/utils/`: + +```ts [server/utils/hello.ts] +export function makeGreeting(name: string) { + return `Hello, ${name}!`; +} +``` + +2. The function is available without importing it: + +```ts [server.ts] +import { defineHandler } from "nitro/h3"; +import { makeGreeting } from "./server/utils/hello.ts"; + +export default defineHandler(() => `

${makeGreeting("Nitro")}

`); +``` + +With this setup, any function exported from `server/utils/` becomes globally available. Nitro scans the directory and generates the necessary imports automatically. + + + +## Learn More + +- [Configuration](/docs/configuration) diff --git a/examples/cached-handler/GUIDE.md b/examples/cached-handler/GUIDE.md new file mode 100644 index 0000000000..aca4e49916 --- /dev/null +++ b/examples/cached-handler/GUIDE.md @@ -0,0 +1,21 @@ +This example shows how to cache an expensive operation (a 500 ms delay) and conditionally bypass the cache using a query parameter. On first request, the handler executes and caches the result. Subsequent requests return the cached response instantly until the cache expires or is bypassed. + +## How It Works + +```ts [server.ts] +import { html } from "nitro/h3"; +import { defineCachedHandler } from "nitro/cache"; + +export default defineCachedHandler( + async () => { + await new Promise((resolve) => setTimeout(resolve, 500)); + return html` + Response generated at ${new Date().toISOString()} (took 500ms) +
(skip cache) + `; + }, + { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") } +); +``` + +The handler simulates a slow operation with a 500ms delay. As `defineCachedHandler` wraps it, the response is cached after the first execution. The `shouldBypassCache` option checks for `?skipCache=true` in the URL and when present the cache is skipped and the handler runs fresh. diff --git a/examples/cached-handler/README.md b/examples/cached-handler/README.md new file mode 100644 index 0000000000..4afda0049d --- /dev/null +++ b/examples/cached-handler/README.md @@ -0,0 +1,95 @@ +--- +category: features +icon: i-lucide-clock +--- + +# Cached Handler + +> Cache route responses with configurable bypass logic. + + + +::code-tree{defaultValue="server.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```ts [server.ts] +import { html } from "nitro/h3"; +import { defineCachedHandler } from "nitro/cache"; + +export default defineCachedHandler( + async () => { + await new Promise((resolve) => setTimeout(resolve, 500)); + return html` + Response generated at ${new Date().toISOString()} (took 500ms) +
(skip cache) + `; + }, + { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") } +); +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +:: + + + + + +This example shows how to cache an expensive operation (a 500 ms delay) and conditionally bypass the cache using a query parameter. On first request, the handler executes and caches the result. Subsequent requests return the cached response instantly until the cache expires or is bypassed. + +## How It Works + +```ts [server.ts] +import { html } from "nitro/h3"; +import { defineCachedHandler } from "nitro/cache"; + +export default defineCachedHandler( + async () => { + await new Promise((resolve) => setTimeout(resolve, 500)); + return html` + Response generated at ${new Date().toISOString()} (took 500ms) +
(skip cache) + `; + }, + { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") } +); +``` + +The handler simulates a slow operation with a 500ms delay. As `defineCachedHandler` wraps it, the response is cached after the first execution. The `shouldBypassCache` option checks for `?skipCache=true` in the URL and when present the cache is skipped and the handler runs fresh. + + + +## Learn More + +- [Cache](/docs/cache) +- [Storage](/docs/storage) diff --git a/examples/custom-error-handler/GUIDE.md b/examples/custom-error-handler/GUIDE.md new file mode 100644 index 0000000000..48d898a833 --- /dev/null +++ b/examples/custom-error-handler/GUIDE.md @@ -0,0 +1,32 @@ +This example shows how to intercept all errors and return a custom response format. When any route throws an error, Nitro calls your error handler instead of returning the default error page. + +## Error Handler + +Create an `error.ts` file in your project root to define the global error handler: + +```ts [error.ts] +import { defineErrorHandler } from "nitro"; + +export default defineErrorHandler((error, _event) => { + return new Response(`Custom Error Handler: ${error.message}`, { + status: 500, + headers: { "Content-Type": "text/plain" }, + }); +}); +``` + +The handler receives the thrown error and the H3 event object. You can use the event to access request details like headers, cookies, or the URL path to customize responses per route. + +## Triggering an Error + +The main handler throws an error to demonstrate the custom error handler: + +```ts [server.ts] +import { defineHandler, HTTPError } from "nitro/h3"; + +export default defineHandler(() => { + throw new HTTPError("Example Error!", { status: 500 }); +}); +``` + +When you visit the page, instead of seeing a generic error page, you'll see "Custom Error Handler: Example Error!" because the error handler intercepts the thrown error. diff --git a/examples/custom-error-handler/README.md b/examples/custom-error-handler/README.md new file mode 100644 index 0000000000..fef0ceefb7 --- /dev/null +++ b/examples/custom-error-handler/README.md @@ -0,0 +1,112 @@ +--- +category: features +icon: i-lucide-alert-circle +--- + +# Custom Error Handler + +> Customize error responses with a global error handler. + + + +::code-tree{defaultValue="error.ts" expandAll} + +```ts [error.ts] +import { defineErrorHandler } from "nitro"; + +export default defineErrorHandler((error, _event) => { + return new Response(`Custom Error Handler: ${error.message}`, { + status: 500, + headers: { "Content-Type": "text/plain" }, + }); +}); +``` + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; +// import errorHandler from "./error"; + +export default defineConfig({ + errorHandler: "./error.ts", + // devErrorHandler: errorHandler, +}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```ts [server.ts] +import { defineHandler, HTTPError } from "nitro/h3"; + +export default defineHandler(() => { + throw new HTTPError("Example Error!", { status: 500 }); +}); +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +:: + + + + + +This example shows how to intercept all errors and return a custom response format. When any route throws an error, Nitro calls your error handler instead of returning the default error page. + +## Error Handler + +Create an `error.ts` file in your project root to define the global error handler: + +```ts [error.ts] +import { defineErrorHandler } from "nitro"; + +export default defineErrorHandler((error, _event) => { + return new Response(`Custom Error Handler: ${error.message}`, { + status: 500, + headers: { "Content-Type": "text/plain" }, + }); +}); +``` + +The handler receives the thrown error and the H3 event object. You can use the event to access request details like headers, cookies, or the URL path to customize responses per route. + +## Triggering an Error + +The main handler throws an error to demonstrate the custom error handler: + +```ts [server.ts] +import { defineHandler, HTTPError } from "nitro/h3"; + +export default defineHandler(() => { + throw new HTTPError("Example Error!", { status: 500 }); +}); +``` + +When you visit the page, instead of seeing a generic error page, you'll see "Custom Error Handler: Example Error!" because the error handler intercepts the thrown error. + + + +## Learn More + +- [Server Entry](/docs/server-entry) diff --git a/examples/database/GUIDE.md b/examples/database/GUIDE.md new file mode 100644 index 0000000000..8fd7dccc88 --- /dev/null +++ b/examples/database/GUIDE.md @@ -0,0 +1,57 @@ +Nitro provides a built-in database layer that uses SQL template literals for safe, parameterized queries. This example creates a users table, inserts a record, and queries it back. + +## Querying the Database + +```ts [server.ts] +import { defineHandler } from "nitro/h3"; +import { useDatabase } from "nitro/database"; + +export default defineHandler(async () => { + const db = useDatabase(); + + // Create users table + await db.sql`DROP TABLE IF EXISTS users`; + await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; + + // Add a new user + const userId = String(Math.round(Math.random() * 10_000)); + await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; + + // Query for users + const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`; + + return { + rows, + }; +}); +``` + +Retrieve the database instance using `useDatabase()`. The database can be queried using `db.sql`, and variables like `${userId}` are automatically escaped to prevent SQL injection. + +## Running Migrations with Tasks + +Nitro tasks let you run operations outside of request handlers. For database migrations, create a task file in `tasks/` and run it via the CLI. This keeps schema changes separate from your application code. + +```ts [tasks/db/migrate.ts] +import { defineTask } from "nitro/task"; +import { useDatabase } from "nitro/database"; + +export default defineTask({ + meta: { + description: "Run database migrations", + }, + async run() { + const db = useDatabase(); + + console.log("Running database migrations..."); + + // Create users table + await db.sql`DROP TABLE IF EXISTS users`; + await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; + + return { + result: "Database migrations complete!", + }; + }, +}); +``` diff --git a/examples/database/README.md b/examples/database/README.md new file mode 100644 index 0000000000..379451eafb Binary files /dev/null and b/examples/database/README.md differ diff --git a/examples/elysia/GUIDE.md b/examples/elysia/GUIDE.md new file mode 100644 index 0000000000..ca543c5a64 --- /dev/null +++ b/examples/elysia/GUIDE.md @@ -0,0 +1,15 @@ +## Server Entry + +```ts [server.ts] +import { Elysia } from "elysia"; + +const app = new Elysia(); + +app.get("/", () => "Hello, Elysia with Nitro!"); + +export default app.compile(); +``` + +Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Elysia app handles all incoming requests, giving you full control over routing and middleware. + +Call `app.compile()` before exporting to optimize the router for production. diff --git a/examples/elysia/README.md b/examples/elysia/README.md new file mode 100644 index 0000000000..bedb304ffb --- /dev/null +++ b/examples/elysia/README.md @@ -0,0 +1,84 @@ +--- +category: backend frameworks +icon: i-skill-icons-elysia-dark +--- + +# Elysia + +> Integrate Elysia with Nitro using the server entry. + + + +::code-tree{defaultValue="server.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "build": "nitro build", + "dev": "nitro dev" + }, + "devDependencies": { + "elysia": "^1.4.22", + "nitro": "latest" + } +} +``` + +```ts [server.ts] +import { Elysia } from "elysia"; + +const app = new Elysia(); + +app.get("/", () => "Hello, Elysia with Nitro!"); + +export default app.compile(); +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +:: + + + + + +## Server Entry + +```ts [server.ts] +import { Elysia } from "elysia"; + +const app = new Elysia(); + +app.get("/", () => "Hello, Elysia with Nitro!"); + +export default app.compile(); +``` + +Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Elysia app handles all incoming requests, giving you full control over routing and middleware. + +Call `app.compile()` before exporting to optimize the router for production. + + + +## Learn More + +- [Server Entry](/docs/server-entry) +- [Elysia Documentation](https://elysiajs.com/) diff --git a/examples/express/GUIDE.md b/examples/express/GUIDE.md new file mode 100644 index 0000000000..322fb68aaa --- /dev/null +++ b/examples/express/GUIDE.md @@ -0,0 +1,19 @@ +## Server Entry + +```ts [server.node.ts] +import Express from "express"; + +const app = Express(); + +app.use("/", (_req, res) => { + res.send("Hello from Express with Nitro!"); +}); + +export default app; +``` + +Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry. The Express app handles all incoming requests, giving you full control over routing and middleware. + +::note +The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno. +:: diff --git a/examples/express/README.md b/examples/express/README.md new file mode 100644 index 0000000000..3e008afea4 --- /dev/null +++ b/examples/express/README.md @@ -0,0 +1,91 @@ +--- +category: backend frameworks +icon: i-simple-icons-express +--- + +# Express + +> Integrate Express with Nitro using the server entry. + + + +::code-tree{defaultValue="server.node.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "build": "nitro build", + "dev": "nitro dev" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "express": "^5.2.1", + "nitro": "latest" + } +} +``` + +```ts [server.node.ts] +import Express from "express"; + +const app = Express(); + +app.use("/", (_req, res) => { + res.send("Hello from Express with Nitro!"); +}); + +export default app; +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +:: + + + + + +## Server Entry + +```ts [server.node.ts] +import Express from "express"; + +const app = Express(); + +app.use("/", (_req, res) => { + res.send("Hello from Express with Nitro!"); +}); + +export default app; +``` + +Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry. The Express app handles all incoming requests, giving you full control over routing and middleware. + +::note +The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno. +:: + + + +## Learn More + +- [Server Entry](/docs/server-entry) +- [Express Documentation](https://expressjs.com/) diff --git a/examples/fastify/GUIDE.md b/examples/fastify/GUIDE.md new file mode 100644 index 0000000000..14ad1102dd --- /dev/null +++ b/examples/fastify/GUIDE.md @@ -0,0 +1,21 @@ +## Server Entry + +```ts [server.node.ts] +import Fastify from "fastify"; + +const app = Fastify(); + +app.get("/", () => "Hello, Fastify with Nitro!"); + +await app.ready(); + +export default app.routing; +``` + +Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry. + +Call `await app.ready()` to initialize all registered plugins before exporting. Export `app.routing` (not `app`) to provide Nitro with the request handler function. + +::note +The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno. +:: diff --git a/examples/fastify/README.md b/examples/fastify/README.md new file mode 100644 index 0000000000..b522a54323 --- /dev/null +++ b/examples/fastify/README.md @@ -0,0 +1,92 @@ +--- +category: backend frameworks +icon: i-simple-icons-fastify +--- + +# Fastify + +> Integrate Fastify with Nitro using the server entry. + + + +::code-tree{defaultValue="server.node.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "build": "nitro build", + "dev": "nitro dev" + }, + "devDependencies": { + "fastify": "^5.7.2", + "nitro": "latest" + } +} +``` + +```ts [server.node.ts] +import Fastify from "fastify"; + +const app = Fastify(); + +app.get("/", () => "Hello, Fastify with Nitro!"); + +await app.ready(); + +export default app.routing; +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +:: + + + + + +## Server Entry + +```ts [server.node.ts] +import Fastify from "fastify"; + +const app = Fastify(); + +app.get("/", () => "Hello, Fastify with Nitro!"); + +await app.ready(); + +export default app.routing; +``` + +Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry. + +Call `await app.ready()` to initialize all registered plugins before exporting. Export `app.routing` (not `app`) to provide Nitro with the request handler function. + +::note +The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno. +:: + + + +## Learn More + +- [Server Entry](/docs/server-entry) +- [Fastify Documentation](https://fastify.dev/) diff --git a/examples/hello-world/GUIDE.md b/examples/hello-world/GUIDE.md new file mode 100644 index 0000000000..60861b543f --- /dev/null +++ b/examples/hello-world/GUIDE.md @@ -0,0 +1,16 @@ +The simplest Nitro server. Export an object with a `fetch` method that receives a standard `Request` and returns a `Response`. No frameworks, no abstractions, just the web platform. + + +## Server Entry + +```ts [server.ts] +export default { + fetch(req: Request) { + return new Response("Nitro Works!"); + }, +}; +``` + +The `fetch` method follows the same signature as Service Workers and Cloudflare Workers. This pattern works across all deployment targets because it uses web standards. + +Add the Nitro plugin to Vite and it handles the rest: dev server, hot reloading, and production builds. diff --git a/examples/hello-world/README.md b/examples/hello-world/README.md new file mode 100644 index 0000000000..500747545f --- /dev/null +++ b/examples/hello-world/README.md @@ -0,0 +1,83 @@ +--- +category: features +icon: i-lucide-sparkles +--- + +# Hello World + +> Minimal Nitro server using the web standard fetch handler. + + + +::code-tree{defaultValue="server.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "build": "nitro build", + "dev": "nitro dev", + "preview": "node .output/server/index.mjs" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```ts [server.ts] +export default { + fetch(req: Request) { + return new Response("Nitro Works!"); + }, +}; +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +:: + + + + + +The simplest Nitro server. Export an object with a `fetch` method that receives a standard `Request` and returns a `Response`. No frameworks, no abstractions, just the web platform. + + +## Server Entry + +```ts [server.ts] +export default { + fetch(req: Request) { + return new Response("Nitro Works!"); + }, +}; +``` + +The `fetch` method follows the same signature as Service Workers and Cloudflare Workers. This pattern works across all deployment targets because it uses web standards. + +Add the Nitro plugin to Vite and it handles the rest: dev server, hot reloading, and production builds. + + + +## Learn More + +- [Server Entry](/docs/server-entry) +- [Configuration](/docs/configuration) diff --git a/examples/hono/GUIDE.md b/examples/hono/GUIDE.md new file mode 100644 index 0000000000..6079b7840b --- /dev/null +++ b/examples/hono/GUIDE.md @@ -0,0 +1,17 @@ +## Server Entry + +```ts [server.ts] +import { Hono } from "hono"; + +const app = new Hono(); + +app.get("/", (c) => { + return c.text("Hello, Hono with Nitro!"); +}); + +export default app; +``` + +Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Hono app handles all incoming requests, giving you full control over routing and middleware. + +Hono is cross-runtime compatible, so this server entry works across all Nitro deployment targets including Node.js, Deno, Bun, and Cloudflare Workers. diff --git a/examples/hono/README.md b/examples/hono/README.md new file mode 100644 index 0000000000..220fee1c6d --- /dev/null +++ b/examples/hono/README.md @@ -0,0 +1,88 @@ +--- +category: backend frameworks +icon: i-logos-hono +--- + +# Hono + +> Integrate Hono with Nitro using the server entry. + + + +::code-tree{defaultValue="server.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "build": "nitro build", + "dev": "nitro dev" + }, + "devDependencies": { + "hono": "^4.11.7", + "nitro": "latest" + } +} +``` + +```ts [server.ts] +import { Hono } from "hono"; + +const app = new Hono(); + +app.get("/", (c) => { + return c.text("Hello, Hono with Nitro!"); +}); + +export default app; +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +:: + + + + + +## Server Entry + +```ts [server.ts] +import { Hono } from "hono"; + +const app = new Hono(); + +app.get("/", (c) => { + return c.text("Hello, Hono with Nitro!"); +}); + +export default app; +``` + +Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Hono app handles all incoming requests, giving you full control over routing and middleware. + +Hono is cross-runtime compatible, so this server entry works across all Nitro deployment targets including Node.js, Deno, Bun, and Cloudflare Workers. + + + +## Learn More + +- [Server Entry](/docs/server-entry) +- [Hono Documentation](https://hono.dev/) diff --git a/examples/import-alias/GUIDE.md b/examples/import-alias/GUIDE.md new file mode 100644 index 0000000000..22274bc8c6 --- /dev/null +++ b/examples/import-alias/GUIDE.md @@ -0,0 +1,21 @@ +Import aliases like `~` and `#` let you reference modules with shorter paths instead of relative imports. + +## Importing Using Aliases + +```ts [server/routes/index.ts] +import { sum } from "~server/utils/math.ts"; + +import { rand } from "#server/utils/math.ts"; + +export default () => { + const [a, b] = [rand(1, 10), rand(1, 10)]; + const result = sum(a, b); + return `The sum of ${a} + ${b} = ${result}`; +}; +``` + +The route imports the `sum` function using `~server/` and `rand` using `#server/`. Both resolve to the same `server/utils/math.ts` file. The handler generates two random numbers and returns their sum. + +## Configuration + +Aliases can be configured in `package.json` imports field or `nitro.config.ts`. diff --git a/examples/import-alias/README.md b/examples/import-alias/README.md new file mode 100644 index 0000000000..892e556c09 --- /dev/null +++ b/examples/import-alias/README.md @@ -0,0 +1,114 @@ +--- +category: config +icon: i-lucide-at-sign +--- + +# Import Alias + +> Custom import aliases for cleaner module paths. + + + +::code-tree{defaultValue="server/routes/index.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: true, + experimental: { + tsconfigPaths: true, + }, +}); +``` + +```json [package.json] +{ + "type": "module", + "imports": { + "#server/*": "./server/*" + }, + "scripts": { + "build": "nitro build", + "dev": "nitro dev", + "preview": "node .output/server/index.mjs" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig", + "compilerOptions": { + "paths": { + "~server/*": ["./server/*"] + } + } +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +```ts [server/routes/index.ts] +import { sum } from "~server/utils/math.ts"; + +import { rand } from "#server/utils/math.ts"; + +export default () => { + const [a, b] = [rand(1, 10), rand(1, 10)]; + const result = sum(a, b); + return `The sum of ${a} + ${b} = ${result}`; +}; +``` + +```ts [server/utils/math.ts] +export function rand(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +export function sum(a: number, b: number): number { + return a + b; +} +``` + +:: + + + + + +Import aliases like `~` and `#` let you reference modules with shorter paths instead of relative imports. + +## Importing Using Aliases + +```ts [server/routes/index.ts] +import { sum } from "~server/utils/math.ts"; + +import { rand } from "#server/utils/math.ts"; + +export default () => { + const [a, b] = [rand(1, 10), rand(1, 10)]; + const result = sum(a, b); + return `The sum of ${a} + ${b} = ${result}`; +}; +``` + +The route imports the `sum` function using `~server/` and `rand` using `#server/`. Both resolve to the same `server/utils/math.ts` file. The handler generates two random numbers and returns their sum. + +## Configuration + +Aliases can be configured in `package.json` imports field or `nitro.config.ts`. + + + +## Learn More + +- [Configuration](/docs/configuration) diff --git a/examples/middleware/GUIDE.md b/examples/middleware/GUIDE.md new file mode 100644 index 0000000000..1490ab5133 --- /dev/null +++ b/examples/middleware/GUIDE.md @@ -0,0 +1,30 @@ +Middleware functions run before route handlers on every request. They can modify the request, add context, or return early responses. + +## Defining Middleware + +Create files in `server/middleware/`. They run in alphabetical order: + +```ts [server/middleware/auth.ts] +import { defineMiddleware } from "nitro/h3"; + +export default defineMiddleware((event) => { + event.context.auth = { name: "User " + Math.round(Math.random() * 100) }; +}); +``` + +Middleware can: +- Add data to `event.context` for use in handlers +- Return a response early to short-circuit the request +- Modify request headers or other properties + +## Accessing Context in Handlers + +Data added to `event.context` in middleware is available in all subsequent handlers: + +```ts [server.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler((event) => ({ + auth: event.context.auth, +})); +``` diff --git a/examples/middleware/README.md b/examples/middleware/README.md new file mode 100644 index 0000000000..610ebe19a8 --- /dev/null +++ b/examples/middleware/README.md @@ -0,0 +1,105 @@ +--- +category: features +icon: i-lucide-layers +--- + +# Middleware + +> Request middleware for authentication, logging, and request modification. + + + +::code-tree{defaultValue="server/middleware/auth.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: true, +}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```ts [server.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler((event) => ({ + auth: event.context.auth, +})); +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +```ts [server/middleware/auth.ts] +import { defineMiddleware } from "nitro/h3"; + +export default defineMiddleware((event) => { + event.context.auth = { name: "User " + Math.round(Math.random() * 100) }; +}); +``` + +:: + + + + + +Middleware functions run before route handlers on every request. They can modify the request, add context, or return early responses. + +## Defining Middleware + +Create files in `server/middleware/`. They run in alphabetical order: + +```ts [server/middleware/auth.ts] +import { defineMiddleware } from "nitro/h3"; + +export default defineMiddleware((event) => { + event.context.auth = { name: "User " + Math.round(Math.random() * 100) }; +}); +``` + +Middleware can: +- Add data to `event.context` for use in handlers +- Return a response early to short-circuit the request +- Modify request headers or other properties + +## Accessing Context in Handlers + +Data added to `event.context` in middleware is available in all subsequent handlers: + +```ts [server.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler((event) => ({ + auth: event.context.auth, +})); +``` + + + +## Learn More + +- [Routing](/docs/routing) diff --git a/examples/mono-jsx/GUIDE.md b/examples/mono-jsx/GUIDE.md new file mode 100644 index 0000000000..e5c9ec899b --- /dev/null +++ b/examples/mono-jsx/GUIDE.md @@ -0,0 +1,11 @@ +## Server Entry + +```tsx [server.tsx] +export default () => ( + +

Nitro + mongo-jsx works!

+ +); +``` + +Nitro auto-detects `server.tsx` and uses mono-jsx to transform JSX into HTML. Export a function that returns JSX, and Nitro sends the rendered HTML as the response. diff --git a/examples/mono-jsx/README.md b/examples/mono-jsx/README.md new file mode 100644 index 0000000000..30239e9d72 --- /dev/null +++ b/examples/mono-jsx/README.md @@ -0,0 +1,82 @@ +--- +category: server side rendering +icon: i-lucide-brackets +--- + +# Mono JSX + +> Server-side JSX rendering in Nitro with mono-jsx. + + + +::code-tree{defaultValue="server.tsx" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "mono-jsx": "latest", + "nitro": "latest" + } +} +``` + +```tsx [server.tsx] +export default () => ( + +

Nitro + mongo-jsx works!

+ +); +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "mono-jsx" + } +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +:: + + + + + +## Server Entry + +```tsx [server.tsx] +export default () => ( + +

Nitro + mongo-jsx works!

+ +); +``` + +Nitro auto-detects `server.tsx` and uses mono-jsx to transform JSX into HTML. Export a function that returns JSX, and Nitro sends the rendered HTML as the response. + + + +## Learn More + +- [Renderer](/docs/renderer) +- [mono-jsx](https://github.com/aspect-dev/mono-jsx) diff --git a/examples/nano-jsx/GUIDE.md b/examples/nano-jsx/GUIDE.md new file mode 100644 index 0000000000..916cd86afa --- /dev/null +++ b/examples/nano-jsx/GUIDE.md @@ -0,0 +1,12 @@ +## Server Entry + +```tsx [server.tsx] +import { defineHandler, html } from "h3"; +import { renderSSR } from "nano-jsx"; + +export default defineHandler(() => { + return html(renderSSR(() =>

Nitro + nano-jsx works!

)); +}); +``` + +Nitro auto-detects `server.tsx` and uses it as the server entry. Use `renderSSR` from nano-jsx to convert JSX into an HTML string. The `html` helper from H3 sets the correct content type header. diff --git a/examples/nano-jsx/README.md b/examples/nano-jsx/README.md new file mode 100644 index 0000000000..321b61fc0c --- /dev/null +++ b/examples/nano-jsx/README.md @@ -0,0 +1,84 @@ +--- +category: server side rendering +icon: i-lucide-brackets +--- + +# Nano JSX + +> Server-side JSX rendering in Nitro with nano-jsx. + + + +::code-tree{defaultValue="server.tsx" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nano-jsx": "^0.2.1", + "nitro": "latest" + } +} +``` + +```tsx [server.tsx] +import { defineHandler, html } from "h3"; +import { renderSSR } from "nano-jsx"; + +export default defineHandler(() => { + return html(renderSSR(() =>

Nitro + nano-jsx works!

)); +}); +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "nano-jsx/esm" + } +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +:: + + + + + +## Server Entry + +```tsx [server.tsx] +import { defineHandler, html } from "h3"; +import { renderSSR } from "nano-jsx"; + +export default defineHandler(() => { + return html(renderSSR(() =>

Nitro + nano-jsx works!

)); +}); +``` + +Nitro auto-detects `server.tsx` and uses it as the server entry. Use `renderSSR` from nano-jsx to convert JSX into an HTML string. The `html` helper from H3 sets the correct content type header. + + + +## Learn More + +- [Renderer](/docs/renderer) +- [nano-jsx](https://nanojsx.io/) diff --git a/examples/plugins/GUIDE.md b/examples/plugins/GUIDE.md new file mode 100644 index 0000000000..51420d853c --- /dev/null +++ b/examples/plugins/GUIDE.md @@ -0,0 +1,27 @@ +Plugins let you hook into Nitro's runtime lifecycle. This example shows a plugin that modifies the `Content-Type` header on every response. Create files in `server/plugins/` and they're automatically loaded at startup. + +## Defining a Plugin + +```ts [server/plugins/test.ts] +import { definePlugin } from "nitro"; +import { useNitroHooks } from "nitro/app"; + +export default definePlugin((nitroApp) => { + const hooks = useNitroHooks(); + hooks.hook("response", (event) => { + event.headers.set("content-type", "html; charset=utf-8"); + }); +}); +``` + +The plugin uses `useNitroHooks()` to access the hooks system, then registers a `response` hook that runs after every request. Here it sets the content type to HTML, but you could log requests, add security headers, or modify responses in any way. + +## Main Handler + +```ts [server.ts] +import { eventHandler } from "h3"; + +export default eventHandler(() => "

Hello Nitro!

"); +``` + +The handler returns HTML without setting a content type. The plugin automatically adds the correct `Content-Type: html; charset=utf-8` header to the response. diff --git a/examples/plugins/README.md b/examples/plugins/README.md new file mode 100644 index 0000000000..7fa3479a56 --- /dev/null +++ b/examples/plugins/README.md @@ -0,0 +1,105 @@ +--- +category: features +icon: i-lucide-plug +--- + +# Plugins + +> Extend Nitro with custom plugins for hooks and lifecycle events. + + + +::code-tree{defaultValue="server/plugins/test.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: true, +}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```ts [server.ts] +import { eventHandler } from "h3"; + +export default eventHandler(() => "

Hello Nitro!

"); +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +```ts [server/plugins/test.ts] +import { definePlugin } from "nitro"; +import { useNitroHooks } from "nitro/app"; + +export default definePlugin((nitroApp) => { + const hooks = useNitroHooks(); + hooks.hook("response", (event) => { + event.headers.set("content-type", "html; charset=utf-8"); + }); +}); +``` + +:: + + + + + +Plugins let you hook into Nitro's runtime lifecycle. This example shows a plugin that modifies the `Content-Type` header on every response. Create files in `server/plugins/` and they're automatically loaded at startup. + +## Defining a Plugin + +```ts [server/plugins/test.ts] +import { definePlugin } from "nitro"; +import { useNitroHooks } from "nitro/app"; + +export default definePlugin((nitroApp) => { + const hooks = useNitroHooks(); + hooks.hook("response", (event) => { + event.headers.set("content-type", "html; charset=utf-8"); + }); +}); +``` + +The plugin uses `useNitroHooks()` to access the hooks system, then registers a `response` hook that runs after every request. Here it sets the content type to HTML, but you could log requests, add security headers, or modify responses in any way. + +## Main Handler + +```ts [server.ts] +import { eventHandler } from "h3"; + +export default eventHandler(() => "

Hello Nitro!

"); +``` + +The handler returns HTML without setting a content type. The plugin automatically adds the correct `Content-Type: html; charset=utf-8` header to the response. + + + +## Learn More + +- [Plugins](/docs/plugins) +- [Lifecycle](/docs/lifecycle) diff --git a/examples/renderer/GUIDE.md b/examples/renderer/GUIDE.md new file mode 100644 index 0000000000..57728650b4 --- /dev/null +++ b/examples/renderer/GUIDE.md @@ -0,0 +1,39 @@ +Create a custom renderer that generates HTML responses with data from API routes. Use Nitro's internal `fetch` to call routes without network overhead. + +## Renderer + +```ts [renderer.ts] +import { fetch } from "nitro"; + +export default async function renderer({ url }: { req: Request; url: URL }) { + const apiRes = await fetch("/api/hello").then((res) => res.text()); + return new Response( + /* html */ ` + + + Custom Renderer + + +

Hello from custom renderer!

+

Current path: ${url.pathname}

+

API says: ${apiRes}

+ + `, + { headers: { "content-type": "text/html; charset=utf-8" } } + ); +} +``` + +Nitro auto-detects `renderer.ts` in your project root and uses it for all non-API routes. The renderer function receives the request URL and returns a `Response`. + +Use `fetch` from `nitro` to call API routes without network overhead—these requests stay in-process. + +## API Route + +```ts [api/hello.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Nitro is amazing!"); +``` + +Define API routes in the `api/` directory. When the renderer calls `fetch("/api/hello")`, this handler runs and returns its response. diff --git a/examples/renderer/README.md b/examples/renderer/README.md new file mode 100644 index 0000000000..6d44051328 --- /dev/null +++ b/examples/renderer/README.md @@ -0,0 +1,127 @@ +--- +category: server side rendering +icon: i-lucide-code +--- + +# Custom Renderer + +> Build a custom HTML renderer in Nitro with server-side data fetching. + + + +::code-tree{defaultValue="renderer.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: "./", + renderer: { handler: "./renderer" }, +}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```ts [renderer.ts] +import { fetch } from "nitro"; + +export default async function renderer({ url }: { req: Request; url: URL }) { + const apiRes = await fetch("/api/hello").then((res) => res.text()); + return new Response( + /* html */ ` + + + Custom Renderer + + +

Hello from custom renderer!

+

Current path: ${url.pathname}

+

API says: ${apiRes}

+ + `, + { headers: { "content-type": "text/html; charset=utf-8" } } + ); +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +```ts [api/hello.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Nitro is amazing!"); +``` + +:: + + + + + +Create a custom renderer that generates HTML responses with data from API routes. Use Nitro's internal `fetch` to call routes without network overhead. + +## Renderer + +```ts [renderer.ts] +import { fetch } from "nitro"; + +export default async function renderer({ url }: { req: Request; url: URL }) { + const apiRes = await fetch("/api/hello").then((res) => res.text()); + return new Response( + /* html */ ` + + + Custom Renderer + + +

Hello from custom renderer!

+

Current path: ${url.pathname}

+

API says: ${apiRes}

+ + `, + { headers: { "content-type": "text/html; charset=utf-8" } } + ); +} +``` + +Nitro auto-detects `renderer.ts` in your project root and uses it for all non-API routes. The renderer function receives the request URL and returns a `Response`. + +Use `fetch` from `nitro` to call API routes without network overhead—these requests stay in-process. + +## API Route + +```ts [api/hello.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Nitro is amazing!"); +``` + +Define API routes in the `api/` directory. When the renderer calls `fetch("/api/hello")`, this handler runs and returns its response. + + + +## Learn More + +- [Renderer](/docs/renderer) diff --git a/examples/runtime-config/GUIDE.md b/examples/runtime-config/GUIDE.md new file mode 100644 index 0000000000..dafd4dc726 --- /dev/null +++ b/examples/runtime-config/GUIDE.md @@ -0,0 +1,39 @@ +Runtime config lets you define configuration values that can be overridden by environment variables at runtime. + +## Define Config Schema + +Declare your runtime config with default values in `nitro.config.ts`: + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: "./", + runtimeConfig: { + apiKey: "", + }, +}); +``` + +## Access at Runtime + +Use `useRuntimeConfig` to access configuration values in your handlers: + +```ts [server.ts] +import { defineHandler } from "nitro/h3"; +import { useRuntimeConfig } from "nitro/runtime-config"; + +export default defineHandler((event) => { + const runtimeConfig = useRuntimeConfig(); + return { runtimeConfig }; +}); +``` + +## Environment Variables + +Override config values via environment variables prefixed with `NITRO_`: + +```sh [.env] +# NEVER COMMIT SENSITIVE DATA. THIS IS ONLY FOR DEMO PURPOSES. +NITRO_API_KEY=secret-api-key +``` diff --git a/examples/runtime-config/README.md b/examples/runtime-config/README.md new file mode 100644 index 0000000000..19ae051839 --- /dev/null +++ b/examples/runtime-config/README.md @@ -0,0 +1,121 @@ +--- +category: config +icon: i-lucide-settings +--- + +# Runtime Config + +> Environment-aware configuration with runtime access. + + + +::code-tree{defaultValue="nitro.config.ts" expandAll} + +```text [.env] +# NEVER COMMIT SENSITIVE DATA. THIS IS ONLY FOR DEMO PURPOSES. +NITRO_API_KEY=secret-api-key +``` + +```text [.gitignore] +# THIS IS ONLY FOR DEMO. DO NOT COMMIT SENSITIVE DATA IN REAL PROJECTS +!.env +``` + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: "./", + runtimeConfig: { + apiKey: "", + }, +}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```ts [server.ts] +import { defineHandler } from "nitro/h3"; +import { useRuntimeConfig } from "nitro/runtime-config"; + +export default defineHandler((event) => { + const runtimeConfig = useRuntimeConfig(); + return { runtimeConfig }; +}); +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +:: + + + + + +Runtime config lets you define configuration values that can be overridden by environment variables at runtime. + +## Define Config Schema + +Declare your runtime config with default values in `nitro.config.ts`: + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: "./", + runtimeConfig: { + apiKey: "", + }, +}); +``` + +## Access at Runtime + +Use `useRuntimeConfig` to access configuration values in your handlers: + +```ts [server.ts] +import { defineHandler } from "nitro/h3"; +import { useRuntimeConfig } from "nitro/runtime-config"; + +export default defineHandler((event) => { + const runtimeConfig = useRuntimeConfig(); + return { runtimeConfig }; +}); +``` + +## Environment Variables + +Override config values via environment variables prefixed with `NITRO_`: + +```sh [.env] +# NEVER COMMIT SENSITIVE DATA. THIS IS ONLY FOR DEMO PURPOSES. +NITRO_API_KEY=secret-api-key +``` + + + +## Learn More + +- [Configuration](/docs/configuration) diff --git a/examples/server-fetch/GUIDE.md b/examples/server-fetch/GUIDE.md new file mode 100644 index 0000000000..08a45c494d --- /dev/null +++ b/examples/server-fetch/GUIDE.md @@ -0,0 +1,22 @@ +When you need one route to call another, use Nitro's `fetch` function instead of the global fetch. It makes internal requests that stay in-process, avoiding network round-trips. The request never leaves the server. + +## Main Route + +```ts [routes/index.ts] +import { defineHandler } from "nitro/h3"; +import { fetch } from "nitro"; + +export default defineHandler(() => fetch("/hello")); +``` + +The index route imports `fetch` from `nitro` (not the global fetch) and calls the `/hello` route. This request is handled internally without going through the network stack. + +## Internal API Route + +```ts [routes/hello.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Hello!"); +``` + +A simple route that returns "Hello!". When the index route calls `fetch("/hello")`, this handler runs and its response is returned directly. diff --git a/examples/server-fetch/README.md b/examples/server-fetch/README.md new file mode 100644 index 0000000000..984d9eea82 --- /dev/null +++ b/examples/server-fetch/README.md @@ -0,0 +1,101 @@ +--- +category: features +icon: i-lucide-arrow-right-left +--- + +# Server Fetch + +> Internal server-to-server requests without network overhead. + + + +::code-tree{defaultValue="routes/index.ts" expandAll} + +```ts [nitro.config.ts] +import { defineConfig, serverFetch } from "nitro"; + +export default defineConfig({ + serverDir: "./", + hooks: { + "dev:start": async () => { + const res = await serverFetch("/hello"); + const text = await res.text(); + console.log("Fetched /hello in nitro module:", res.status, text); + }, + }, +}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +```ts [routes/hello.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Hello!"); +``` + +```ts [routes/index.ts] +import { defineHandler } from "nitro/h3"; +import { fetch } from "nitro"; + +export default defineHandler(() => fetch("/hello")); +``` + +:: + + + + + +When you need one route to call another, use Nitro's `fetch` function instead of the global fetch. It makes internal requests that stay in-process, avoiding network round-trips. The request never leaves the server. + +## Main Route + +```ts [routes/index.ts] +import { defineHandler } from "nitro/h3"; +import { fetch } from "nitro"; + +export default defineHandler(() => fetch("/hello")); +``` + +The index route imports `fetch` from `nitro` (not the global fetch) and calls the `/hello` route. This request is handled internally without going through the network stack. + +## Internal API Route + +```ts [routes/hello.ts] +import { defineHandler } from "nitro/h3"; + +export default defineHandler(() => "Hello!"); +``` + +A simple route that returns "Hello!". When the index route calls `fetch("/hello")`, this handler runs and its response is returned directly. + + + +## Learn More + +- [Routing](/docs/routing) diff --git a/examples/shiki/GUIDE.md b/examples/shiki/GUIDE.md new file mode 100644 index 0000000000..a47d8f1561 --- /dev/null +++ b/examples/shiki/GUIDE.md @@ -0,0 +1,56 @@ +Use Shiki for syntax highlighting with TextMate grammars. This example highlights code on the server using Nitro's server scripts feature, which runs JavaScript inside HTML files before sending the response. + +## API Route + +```ts [api/highlight.ts] +import { createHighlighterCore } from "shiki/core"; +import { createOnigurumaEngine } from "shiki/engine/oniguruma"; + +const highlighter = await createHighlighterCore({ + engine: createOnigurumaEngine(import("shiki/wasm")), + themes: [await import("shiki/themes/vitesse-dark.mjs")], + langs: [await import("shiki/langs/ts.mjs")], +}); + +export default async ({ req }: { req: Request }) => { + const code = await req.text(); + const html = await highlighter.codeToHtml(code, { + lang: "ts", + theme: "vitesse-dark", + }); + return new Response(html, { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); +}; +``` + +Create a Shiki highlighter with the Vitesse Dark theme and TypeScript language support. When the API receives a POST request, it reads the code from the request body and returns highlighted HTML. + +## Server-Side Rendering + +```html [index.html] + + + + + + Hello World Snippet + + + +
+
JavaScript
+ +
{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}
+
+ + +``` + +The ` +
{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}
+ + + +``` + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: "./", +}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build" + }, + "devDependencies": { + "nitro": "latest", + "shiki": "^3.21.0" + } +} +``` + +```css [styles.css] +html, +body { + height: 100%; + margin: 0; +} +body { + display: flex; + align-items: center; + justify-content: center; + background: #f6f8fa; + font-family: + system-ui, + -apple-system, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + "Noto Sans", + "Liberation Sans", + sans-serif; +} +.card { + text-align: left; + background: #0b1220; + color: #e6edf3; + padding: 1rem; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(2, 6, 23, 0.2); + max-width: 90%; + width: 520px; +} +.label { + font-size: 12px; + color: #9aa7b2; + margin-bottom: 8px; +} +pre { + margin: 0; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace; + font-size: 14px; + background: transparent; + white-space: pre; + overflow: auto; +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ + plugins: [nitro()], +}); +``` + +```ts [api/highlight.ts] +import { createHighlighterCore } from "shiki/core"; +import { createOnigurumaEngine } from "shiki/engine/oniguruma"; + +const highlighter = await createHighlighterCore({ + engine: createOnigurumaEngine(import("shiki/wasm")), + themes: [await import("shiki/themes/vitesse-dark.mjs")], + langs: [await import("shiki/langs/ts.mjs")], +}); + +export default async ({ req }: { req: Request }) => { + const code = await req.text(); + const html = await highlighter.codeToHtml(code, { + lang: "ts", + theme: "vitesse-dark", + }); + return new Response(html, { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); +}; +``` + +:: + + + + + +Use Shiki for syntax highlighting with TextMate grammars. This example highlights code on the server using Nitro's server scripts feature, which runs JavaScript inside HTML files before sending the response. + +## API Route + +```ts [api/highlight.ts] +import { createHighlighterCore } from "shiki/core"; +import { createOnigurumaEngine } from "shiki/engine/oniguruma"; + +const highlighter = await createHighlighterCore({ + engine: createOnigurumaEngine(import("shiki/wasm")), + themes: [await import("shiki/themes/vitesse-dark.mjs")], + langs: [await import("shiki/langs/ts.mjs")], +}); + +export default async ({ req }: { req: Request }) => { + const code = await req.text(); + const html = await highlighter.codeToHtml(code, { + lang: "ts", + theme: "vitesse-dark", + }); + return new Response(html, { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); +}; +``` + +Create a Shiki highlighter with the Vitesse Dark theme and TypeScript language support. When the API receives a POST request, it reads the code from the request body and returns highlighted HTML. + +## Server-Side Rendering + +```html [index.html] + + + + + + Hello World Snippet + + + +
+
JavaScript
+ +
{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}
+
+ + +``` + +The `. + const [rscStream1, rscStream2] = rscStream.tee(); + + // Deserialize RSC stream back to React VDOM + let payload: Promise | undefined; + function SsrRoot() { + // Deserialization needs to be kicked off inside ReactDOMServer context + // for ReactDOMServer preinit/preloading to work + payload ??= createFromReadableStream(rscStream1); + return React.use(payload).root; + } + + // Render HTML (traditional SSR) + const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index"); + + let htmlStream: ReadableStream; + let status: number | undefined; + + try { + htmlStream = await renderToReadableStream(, { + bootstrapScriptContent: options?.debugNoJS ? undefined : bootstrapScriptContent, + nonce: options?.nonce, + formState: options?.formState, + }); + } catch { + // fallback to render an empty shell and run pure CSR on browser, + // which can replay server component error and trigger error boundary. + status = 500; + htmlStream = await renderToReadableStream( + + + + + , + { + bootstrapScriptContent: + `self.__NO_HYDRATE=1;` + (options?.debugNoJS ? "" : bootstrapScriptContent), + nonce: options?.nonce, + } + ); + } + + let responseStream: ReadableStream = htmlStream; + if (!options?.debugNoJS) { + // Initial RSC stream is injected in HTML stream as + // using utility made by devongovett https://github.com/devongovett/rsc-html-stream + responseStream = responseStream.pipeThrough( + injectRSCPayload(rscStream2, { + nonce: options?.nonce, + }) + ); + } + + return { stream: responseStream, status }; +} +``` + +The SSR entry handles the rendering pipeline. It loads the RSC entry module, duplicates the RSC stream (one for SSR, one for hydration), deserializes the stream back to React VDOM, and renders it to HTML. The RSC payload is injected into the HTML for client hydration. + +## 2. Root Server Component + +```tsx [app/root.tsx] +import "./index.css"; // css import is automatically injected in exported server components +import viteLogo from "./assets/vite.svg"; +import { getServerCounter, updateServerCounter } from "./action.tsx"; +import reactLogo from "./assets/react.svg"; +import nitroLogo from "./assets/nitro.svg"; +import { ClientCounter } from "./client.tsx"; + +export function Root(props: { url: URL }) { + return ( + + + {/* eslint-disable-next-line unicorn/text-encoding-identifier-case */} + + + + Nitro + Vite + RSC + + + + + + ); +} + +function App(props: { url: URL }) { + return ( +
+ +

Vite + RSC + Nitro

+
+ +
+
+
+ +
+
+
Request URL: {props.url?.href}
+
    +
  • + Edit src/client.tsx to test client HMR. +
  • +
  • + Edit src/root.tsx to test server HMR. +
  • +
  • + Visit{" "} + + _.rsc + {" "} + to view RSC stream payload. +
  • +
  • + Visit{" "} + + ?__nojs + {" "} + to test server action without js enabled. +
  • +
+
+ ); +} +``` + +Server components run only on the server. They can import CSS directly, use server-side data, and call server actions. The `ClientCounter` component is imported but runs on the client because it has the `"use client"` directive. + +## 3. Client Component + +```tsx [app/client.tsx] +"use client"; + +import React from "react"; + +export function ClientCounter() { + const [count, setCount] = React.useState(0); + + return ; +} +``` + +The `"use client"` directive marks this as a client component. It hydrates on the browser and handles interactive state. Server components can import and render client components, but client components cannot import server components. diff --git a/examples/vite-rsc/README.md b/examples/vite-rsc/README.md index 66ba6d8b00..6ed5bd41fc 100644 --- a/examples/vite-rsc/README.md +++ b/examples/vite-rsc/README.md @@ -1,5 +1,867 @@ -# Vite + RSC + Nitro Example +--- +category: vite +icon: i-logos-react +--- -Copied from https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc/examples/starter +# Vite RSC -The difference from the original template is to export `default.fetch` handler from `entry.ssr.tsx` instead of `entry.rsc.tsx`. +> React Server Components with Vite and Nitro. + + + +::code-tree{defaultValue="app/root.tsx" expandAll} + +```text [.gitignore] +node_modules +dist +``` + +```json [package.json] +{ + "name": "@vitejs/plugin-rsc-examples-starter", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "@vitejs/plugin-rsc": "^0.5.17", + "nitro": "latest", + "rsc-html-stream": "^0.0.7", + "vite": "beta" + } +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client", "@vitejs/plugin-rsc/types"], + "jsx": "react-jsx" + } +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +import rsc from "@vitejs/plugin-rsc"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [ + nitro(), + rsc({ + serverHandler: false, + entries: { + ssr: "./app/framework/entry.ssr.tsx", + rsc: "./app/framework/entry.rsc.tsx", + }, + }), + react(), + ], + + environments: { + client: { + build: { + rollupOptions: { + input: { index: "./app/framework/entry.browser.tsx" }, + }, + }, + }, + }, +}); +``` + +```tsx [app/action.tsx] +"use server"; + +let serverCounter = 0; + +export async function getServerCounter() { + return serverCounter; +} + +export async function updateServerCounter(change: number) { + serverCounter += change; +} +``` + +```tsx [app/client.tsx] +"use client"; + +import React from "react"; + +export function ClientCounter() { + const [count, setCount] = React.useState(0); + + return ; +} +``` + +```css [app/index.css] +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 1rem; +} + +.read-the-docs { + color: #888; + text-align: left; +} +``` + +```tsx [app/root.tsx] +import "./index.css"; // css import is automatically injected in exported server components +import viteLogo from "./assets/vite.svg"; +import { getServerCounter, updateServerCounter } from "./action.tsx"; +import reactLogo from "./assets/react.svg"; +import nitroLogo from "./assets/nitro.svg"; +import { ClientCounter } from "./client.tsx"; + +export function Root(props: { url: URL }) { + return ( + + + {/* eslint-disable-next-line unicorn/text-encoding-identifier-case */} + + + + Nitro + Vite + RSC + + + + + + ); +} + +function App(props: { url: URL }) { + return ( +
+ +

Vite + RSC + Nitro

+
+ +
+
+
+ +
+
+
Request URL: {props.url?.href}
+
    +
  • + Edit src/client.tsx to test client HMR. +
  • +
  • + Edit src/root.tsx to test server HMR. +
  • +
  • + Visit{" "} + + _.rsc + {" "} + to view RSC stream payload. +
  • +
  • + Visit{" "} + + ?__nojs + {" "} + to test server action without js enabled. +
  • +
+
+ ); +} +``` + +```text [app/assets/nitro.svg] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +```text [app/assets/react.svg] + +``` + +```text [app/assets/vite.svg] + +``` + +```tsx [app/framework/entry.browser.tsx] +import { + createFromReadableStream, + createFromFetch, + setServerCallback, + createTemporaryReferenceSet, + encodeReply, +} from "@vitejs/plugin-rsc/browser"; +import React from "react"; +import { createRoot, hydrateRoot } from "react-dom/client"; +import { rscStream } from "rsc-html-stream/client"; +import { GlobalErrorBoundary } from "./error-boundary"; +import type { RscPayload } from "./entry.rsc"; +import { createRscRenderRequest } from "./request"; + +async function main() { + // Stash `setPayload` function to trigger re-rendering + // from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr) + let setPayload: (v: RscPayload) => void; + + // Deserialize RSC stream back to React VDOM for CSR + const initialPayload = await createFromReadableStream( + // Initial RSC stream is injected in SSR stream as + rscStream + ); + + // Browser root component to (re-)render RSC payload as state + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload); + + React.useEffect(() => { + setPayload = (v) => React.startTransition(() => setPayload_(v)); + }, [setPayload_]); + + // Re-fetch/render on client side navigation + React.useEffect(() => { + return listenNavigation(() => fetchRscPayload()); + }, []); + + return payload.root; + } + + // Re-fetch RSC and trigger re-rendering + async function fetchRscPayload() { + const renderRequest = createRscRenderRequest(globalThis.location.href); + const payload = await createFromFetch(fetch(renderRequest)); + setPayload(payload); + } + + // Register a handler which will be internally called by React + // on server function request after hydration. + setServerCallback(async (id, args) => { + const temporaryReferences = createTemporaryReferenceSet(); + const renderRequest = createRscRenderRequest(globalThis.location.href, { + id, + body: await encodeReply(args, { temporaryReferences }), + }); + const payload = await createFromFetch(fetch(renderRequest), { + temporaryReferences, + }); + setPayload(payload); + const { ok, data } = payload.returnValue!; + if (!ok) throw data; + return data; + }); + + // Hydration + const browserRoot = ( + + + + + + ); + if ("__NO_HYDRATE" in globalThis) { + createRoot(document).render(browserRoot); + } else { + hydrateRoot(document, browserRoot, { + formState: initialPayload.formState, + }); + } + + // Implement server HMR by triggering re-fetch/render of RSC upon server code change + if (import.meta.hot) { + import.meta.hot.on("rsc:update", () => { + fetchRscPayload(); + }); + } +} + +// A little helper to setup events interception for client side navigation +function listenNavigation(onNavigation: () => void) { + globalThis.addEventListener("popstate", onNavigation); + + const oldPushState = globalThis.history.pushState; + globalThis.history.pushState = function (...args) { + const res = oldPushState.apply(this, args); + onNavigation(); + return res; + }; + + const oldReplaceState = globalThis.history.replaceState; + globalThis.history.replaceState = function (...args) { + const res = oldReplaceState.apply(this, args); + onNavigation(); + return res; + }; + + function onClick(e: MouseEvent) { + const link = (e.target as Element).closest("a"); + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === "_self") && + link.origin === location.origin && + !link.hasAttribute("download") && + e.button === 0 && // left clicks only + !e.metaKey && // open in new tab (mac) + !e.ctrlKey && // open in new tab (windows) + !e.altKey && // download + !e.shiftKey && + !e.defaultPrevented + ) { + e.preventDefault(); + history.pushState(null, "", link.href); + } + } + document.addEventListener("click", onClick); + + return () => { + document.removeEventListener("click", onClick); + globalThis.removeEventListener("popstate", onNavigation); + globalThis.history.pushState = oldPushState; + globalThis.history.replaceState = oldReplaceState; + }; +} + +// eslint-disable-next-line unicorn/prefer-top-level-await +main(); +``` + +```tsx [app/framework/entry.rsc.tsx] +import { + renderToReadableStream, + createTemporaryReferenceSet, + decodeReply, + loadServerAction, + decodeAction, + decodeFormState, +} from "@vitejs/plugin-rsc/rsc"; +import type { ReactFormState } from "react-dom/client"; +import { Root } from "../root.tsx"; +import { parseRenderRequest } from "./request.tsx"; + +// The schema of payload which is serialized into RSC stream on rsc environment +// and deserialized on ssr/client environments. +export type RscPayload = { + // this demo renders/serializes/deserializes entire root html element + // but this mechanism can be changed to render/fetch different parts of components + // based on your own route conventions. + root: React.ReactNode; + + // Server action return value of non-progressive enhancement case + returnValue?: { ok: boolean; data: unknown }; + + // Server action form state (e.g. useActionState) of progressive enhancement case + formState?: ReactFormState; +}; + +// The plugin by default assumes `rsc` entry having default export of request handler. +// however, how server entries are executed can be customized by registering own server handler. +export default async function handler(request: Request): Promise { + // Differentiate RSC, SSR, action, etc. + const renderRequest = parseRenderRequest(request); + request = renderRequest.request; + + // Handle server function request + let returnValue: RscPayload["returnValue"] | undefined; + let formState: ReactFormState | undefined; + let temporaryReferences: unknown | undefined; + let actionStatus: number | undefined; + + if (renderRequest.isAction === true) { + if (renderRequest.actionId) { + // Action is called via `ReactClient.setServerCallback`. + const contentType = request.headers.get("content-type"); + const body = contentType?.startsWith("multipart/form-data") + ? await request.formData() + : await request.text(); + temporaryReferences = createTemporaryReferenceSet(); + const args = await decodeReply(body, { temporaryReferences }); + const action = await loadServerAction(renderRequest.actionId); + try { + // eslint-disable-next-line prefer-spread + const data = await action.apply(null, args); + returnValue = { ok: true, data }; + } catch (error_) { + returnValue = { ok: false, data: error_ }; + actionStatus = 500; + } + } else { + // Otherwise server function is called via `
` + // before hydration (e.g. when JavaScript is disabled). + // aka progressive enhancement. + const formData = await request.formData(); + const decodedAction = await decodeAction(formData); + try { + const result = await decodedAction(); + formState = await decodeFormState(result, formData); + } catch { + // there's no single general obvious way to surface this error, + // so explicitly return classic 500 response. + return new Response("Internal Server Error: server action failed", { + status: 500, + }); + } + } + } + + // Serialization from React VDOM tree to RSC stream. + // We render RSC stream after handling server function request + // so that new render reflects updated state from server function call + // to achieve single round trip to mutate and fetch from server. + const rscPayload: RscPayload = { + root: , + formState, + returnValue, + }; + + const rscOptions = { temporaryReferences }; + const rscStream = renderToReadableStream(rscPayload, rscOptions); + + // Respond RSC stream without HTML rendering as decided by `RenderRequest` + if (renderRequest.isRsc) { + return new Response(rscStream, { + status: actionStatus, + headers: { + "content-type": "text/x-component;charset=utf-8", + }, + }); + } + + // Delegate to SSR environment for HTML rendering. + // The plugin provides `loadModule` helper to allow loading SSR environment entry module + // in RSC environment. however this can be customized by implementing own runtime communication + // e.g. `@cloudflare/vite-plugin`'s service binding. + const ssrEntryModule = await import.meta.viteRsc.loadModule( + "ssr", + "index" + ); + + const ssrResult = await ssrEntryModule.renderHTML(rscStream, { + formState, + // Allow quick simulation of JavaScript disabled browser + debugNoJS: renderRequest.url.searchParams.has("__nojs"), + }); + + // Respond HTML + return new Response(ssrResult.stream, { + status: ssrResult.status, + headers: { + "Content-Type": "text/html", + }, + }); +} + +if (import.meta.hot) { + import.meta.hot.accept(); +} +``` + +```tsx [app/framework/entry.ssr.tsx] +import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; +import React from "react"; +import type { ReactFormState } from "react-dom/client"; +import { renderToReadableStream } from "react-dom/server.edge"; +import { injectRSCPayload } from "rsc-html-stream/server"; +import type { RscPayload } from "./entry.rsc"; + +export default { + fetch: async (request: Request) => { + const rscEntryModule = await import.meta.viteRsc.loadModule( + "rsc", + "index" + ); + return rscEntryModule.default(request); + }, +}; + +export async function renderHTML( + rscStream: ReadableStream, + options: { + formState?: ReactFormState; + nonce?: string; + debugNoJS?: boolean; + } +): Promise<{ stream: ReadableStream; status?: number }> { + // Duplicate one RSC stream into two. + // - one for SSR (ReactClient.createFromReadableStream below) + // - another for browser hydration payload by injecting . + const [rscStream1, rscStream2] = rscStream.tee(); + + // Deserialize RSC stream back to React VDOM + let payload: Promise | undefined; + function SsrRoot() { + // Deserialization needs to be kicked off inside ReactDOMServer context + // for ReactDOMServer preinit/preloading to work + payload ??= createFromReadableStream(rscStream1); + return React.use(payload).root; + } + + // Render HTML (traditional SSR) + const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index"); + + let htmlStream: ReadableStream; + let status: number | undefined; + + try { + htmlStream = await renderToReadableStream(, { + bootstrapScriptContent: options?.debugNoJS ? undefined : bootstrapScriptContent, + nonce: options?.nonce, + formState: options?.formState, + }); + } catch { + // fallback to render an empty shell and run pure CSR on browser, + // which can replay server component error and trigger error boundary. + status = 500; + htmlStream = await renderToReadableStream( + + + + + , + { + bootstrapScriptContent: + `self.__NO_HYDRATE=1;` + (options?.debugNoJS ? "" : bootstrapScriptContent), + nonce: options?.nonce, + } + ); + } + + let responseStream: ReadableStream = htmlStream; + if (!options?.debugNoJS) { + // Initial RSC stream is injected in HTML stream as + // using utility made by devongovett https://github.com/devongovett/rsc-html-stream + responseStream = responseStream.pipeThrough( + injectRSCPayload(rscStream2, { + nonce: options?.nonce, + }) + ); + } + + return { stream: responseStream, status }; +} +``` + +```tsx [app/framework/error-boundary.tsx] +"use client"; + +import React from "react"; + +// Minimal ErrorBoundary example to handle errors globally on browser +export function GlobalErrorBoundary(props: { children?: React.ReactNode }) { + return {props.children}; +} + +// https://github.com/vercel/next.js/blob/33f8428f7066bf8b2ec61f025427ceb2a54c4bdf/packages/next/src/client/components/error-boundary.tsx +// https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary +class ErrorBoundary extends React.Component<{ + children?: React.ReactNode; + errorComponent: React.FC<{ + error: Error; + reset: () => void; + }>; +}> { + override state: { error?: Error } = {}; + + static getDerivedStateFromError(error: Error) { + return { error }; + } + + reset = () => { + this.setState({ error: null }); + }; + + override render() { + const error = this.state.error; + if (error) { + return ; + } + return this.props.children; + } +} + +// https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/build/webpack/loaders/next-app-loader.ts#L73 +// https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/client/components/error-boundary.tsx#L145 +function DefaultGlobalErrorPage(props: { error: Error; reset: () => void }) { + return ( + + + Unexpected Error + + +

Caught an unexpected error

+
+          Error:{" "}
+          {import.meta.env.DEV && "message" in props.error ? props.error.message : "(Unknown)"}
+        
+ + + + ); +} +``` + +```tsx [app/framework/request.tsx] +// Framework conventions (arbitrary choices for this demo): +// - Use `_.rsc` URL suffix to differentiate RSC requests from SSR requests +// - Use `x-rsc-action` header to pass server action ID +const URL_POSTFIX = "_.rsc"; +const HEADER_ACTION_ID = "x-rsc-action"; + +// Parsed request information used to route between RSC/SSR rendering and action handling. +// Created by parseRenderRequest() from incoming HTTP requests. +type RenderRequest = { + isRsc: boolean; // true if request should return RSC payload (via _.rsc suffix) + isAction: boolean; // true if this is a server action call (POST request) + actionId?: string; // server action ID from x-rsc-action header + request: Request; // normalized Request with _.rsc suffix removed from URL + url: URL; // normalized URL with _.rsc suffix removed +}; + +export function createRscRenderRequest( + urlString: string, + action?: { id: string; body: BodyInit } +): Request { + const url = new URL(urlString); + url.pathname += URL_POSTFIX; + const headers = new Headers(); + if (action) { + headers.set(HEADER_ACTION_ID, action.id); + } + return new Request(url.toString(), { + method: action ? "POST" : "GET", + headers, + body: action?.body, + }); +} + +export function parseRenderRequest(request: Request): RenderRequest { + const url = new URL(request.url); + const isAction = request.method === "POST"; + if (url.pathname.endsWith(URL_POSTFIX)) { + url.pathname = url.pathname.slice(0, -URL_POSTFIX.length); + const actionId = request.headers.get(HEADER_ACTION_ID) || undefined; + if (request.method === "POST" && !actionId) { + throw new Error("Missing action id header for RSC action request"); + } + return { + isRsc: true, + isAction, + actionId, + request: new Request(url, request), + url, + }; + } else { + return { + isRsc: false, + isAction, + request, + url, + }; + } +} +``` + +:: + + + + + + + +## Learn More + +- [React Server Components](https://react.dev/reference/rsc/server-components) diff --git a/examples/vite-ssr-html/GUIDE.md b/examples/vite-ssr-html/GUIDE.md new file mode 100644 index 0000000000..201bc2a689 --- /dev/null +++ b/examples/vite-ssr-html/GUIDE.md @@ -0,0 +1,16 @@ +This example renders an HTML template with server-side data and streams the response word by word. It demonstrates how to use Nitro's Vite SSR integration without a framework. + +## Overview + +1. **Add the Nitro Vite plugin** to enable SSR +2. **Create an HTML template** with a `` comment where server content goes +3. **Create a server entry** that fetches data and returns a stream +4. **Add API routes** for server-side data + +## How It Works + +The `index.html` file contains an `` comment that marks where server-rendered content will be inserted. Nitro replaces this comment with the output from your server entry. + +The server entry exports an object with a `fetch` method. It calls the `/quote` API route using Nitro's internal fetch, then returns a `ReadableStream` that emits the quote text word by word with a 50ms delay between each word. + +The quote route fetches a JSON file of quotes from GitHub, caches the result, and returns a random quote. The server entry calls this route to get content for the page. diff --git a/examples/vite-ssr-html/README.md b/examples/vite-ssr-html/README.md new file mode 100644 index 0000000000..4da0878cac --- /dev/null +++ b/examples/vite-ssr-html/README.md @@ -0,0 +1,233 @@ +--- +category: server side rendering +icon: i-logos-html-5 +--- + +# Vite SSR HTML + +> Server-side rendering with vanilla HTML, Vite, and Nitro. + + + + +::code-tree{defaultValue="app/entry-server.ts" expandAll} + +```html [index.html] + + + + + + Nitro Quotes + + + +
+
+
+ +
+
+ +
+
+ Powered by + Vite + and + Nitro v3. +
+
+ + + + +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite dev", + "preview": "vite preview" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.18", + "nitro": "latest", + "tailwindcss": "^4.1.18", + "vite": "beta" + } +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [ + nitro({ + serverDir: "./", + }), + tailwindcss(), + ], +}); +``` + +```ts [app/entry-server.ts] +import { fetch } from "nitro"; + +export default { + async fetch() { + const quote = (await fetch("/quote").then((res) => res.json())) as { + text: string; + }; + return tokenizedStream(quote.text, 50); + }, +}; + +function tokenizedStream(text: string, delay: number): ReadableStream { + const tokens = text.split(" "); + return new ReadableStream({ + start(controller) { + let index = 0; + function push() { + if (index < tokens.length) { + const word = tokens[index++] + (index < tokens.length ? " " : ""); + controller.enqueue(new TextEncoder().encode(word)); + setTimeout(push, delay); + } else { + controller.close(); + } + } + push(); + }, + }); +} +``` + +```ts [routes/quote.ts] +const QUOTES_URL = + "https://github.com/JamesFT/Database-Quotes-JSON/raw/refs/heads/master/quotes.json"; + +let _quotes: Promise | undefined; + +function getQuotes() { + return (_quotes ??= fetch(QUOTES_URL).then((res) => res.json())) as Promise< + { quoteText: string; quoteAuthor: string }[] + >; +} + +export default async function quotesHandler() { + const quotes = await getQuotes(); + const randomQuote = quotes[Math.floor(Math.random() * quotes.length)]; + return Response.json({ + text: randomQuote.quoteText, + author: randomQuote.quoteAuthor, + }); +} +``` + +:: + + + + + +This example renders an HTML template with server-side data and streams the response word by word. It demonstrates how to use Nitro's Vite SSR integration without a framework. + +## Overview + +1. **Add the Nitro Vite plugin** to enable SSR +2. **Create an HTML template** with a `` comment where server content goes +3. **Create a server entry** that fetches data and returns a stream +4. **Add API routes** for server-side data + +## How It Works + +The `index.html` file contains an `` comment that marks where server-rendered content will be inserted. Nitro replaces this comment with the output from your server entry. + +The server entry exports an object with a `fetch` method. It calls the `/quote` API route using Nitro's internal fetch, then returns a `ReadableStream` that emits the quote text word by word with a 50ms delay between each word. + +The quote route fetches a JSON file of quotes from GitHub, caches the result, and returns a random quote. The server entry calls this route to get content for the page. + + + +## Learn More + +- [Renderer](/docs/renderer) +- [Server Entry](/docs/server-entry) diff --git a/examples/vite-ssr-preact/GUIDE.md b/examples/vite-ssr-preact/GUIDE.md new file mode 100644 index 0000000000..37ebb134a4 --- /dev/null +++ b/examples/vite-ssr-preact/GUIDE.md @@ -0,0 +1,113 @@ +Set up server-side rendering (SSR) with Preact, Vite, and Nitro. This setup enables streaming HTML responses, automatic asset management, and client hydration. + +## Overview + +1. Add the Nitro Vite plugin to your Vite config +2. Configure client and server entry points +3. Create a server entry that renders your app to HTML +4. Create a client entry that hydrates the server-rendered HTML + +## 1. Configure Vite + +Add the Nitro and Preact plugins to your Vite config. Define the `client` environment with your client entry point: + +```js [vite.config.mjs] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; +import preact from "@preact/preset-vite"; + +export default defineConfig({ + plugins: [nitro(), preact()], + environments: { + client: { + build: { + rollupOptions: { + input: "./src/entry-client.tsx", + }, + }, + }, + }, +}); +``` + +The `environments.client` configuration tells Vite which file to use as the browser entry point. Nitro automatically detects the server entry from files named `entry-server` or `server` in common directories. + +## 2. Create the App Component + +Create a shared Preact component that runs on both server and client: + +```tsx [src/app.tsx] +import { useState } from "preact/hooks"; + +export function App() { + const [count, setCount] = useState(0); + return ; +} +``` + +## 3. Create the Server Entry + +The server entry renders your Preact app to a streaming HTML response using `preact-render-to-string/stream`: + +```tsx [src/entry-server.tsx] +import "./styles.css"; +import { renderToReadableStream } from "preact-render-to-string/stream"; +import { App } from "./app.jsx"; + +import clientAssets from "./entry-client?assets=client"; +import serverAssets from "./entry-server?assets=ssr"; + +export default { + async fetch(request: Request) { + const url = new URL(request.url); + const htmlStream = renderToReadableStream(); + return new Response(htmlStream, { + headers: { "Content-Type": "text/html;charset=utf-8" }, + }); + }, +}; + +function Root(props: { url: URL }) { + const assets = clientAssets.merge(serverAssets); + return ( + + + + {assets.css.map((attr: any) => ( + + ))} + {assets.js.map((attr: any) => ( + + ))} + + + +``` + +## 3. Create the App Entry + +Create the main entry that initializes TanStack Router: + +```tsx [src/main.tsx] +import { StrictMode } from "react"; +import ReactDOM from "react-dom/client"; +import { RouterProvider, createRouter } from "@tanstack/react-router"; + +// Import the generated route tree +import { routeTree } from "./routeTree.gen.ts"; + +// Create a new router instance +const router = createRouter({ routeTree }); + +// Register the router instance for type safety +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +// Render the app +const rootElement = document.querySelector("#root")!; +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement); + root.render( + + + + ); +} +``` + +The `routeTree.gen.ts` file is auto-generated from your `routes/` directory structure. The `Register` interface declaration provides full type inference for route paths and params. The `!rootElement.innerHTML` check prevents re-rendering during hot module replacement. + +## 4. Create the Root Route + +The root route (`__root.tsx`) defines your app's layout: + +```tsx [src/routes/__root.tsx] +import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; + +const RootLayout = () => ( + <> +
+ + Home + +
+
+ + + +); + +export const Route = createRootRoute({ component: RootLayout }); +``` + +Use `Link` for type-safe navigation with active state styling. The `Outlet` component renders child routes. Include `TanStackRouterDevtools` for development tools (automatically removed in production). + +## 5. Create Page Routes + +Page routes use `createFileRoute` and can include loaders: + +```tsx [src/routes/index.tsx] +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ + loader: async () => { + const r = await fetch("/api/hello"); + return r.json(); + }, + component: Index, +}); + +function Index() { + const r = Route.useLoaderData(); + + return ( +
+

{JSON.stringify(r)}

+
+ ); +} +``` + +Fetch data before rendering with the `loader` function—data is available via `Route.useLoaderData()`. File paths determine URL paths: `routes/index.tsx` maps to `/`, `routes/about.tsx` to `/about`, and `routes/users/$id.tsx` to `/users/:id`. diff --git a/examples/vite-ssr-tsr-react/README.md b/examples/vite-ssr-tsr-react/README.md new file mode 100644 index 0000000000..6d1d414c9a --- /dev/null +++ b/examples/vite-ssr-tsr-react/README.md @@ -0,0 +1,457 @@ +--- +category: server side rendering +icon: i-simple-icons-tanstack +--- + +# SSR with TanStack Router + +> Client-side routing with TanStack Router in Nitro using Vite. + + + +::code-tree{defaultValue="src/main.tsx" expandAll} + +```html [index.html] + + + + + + Nitro + TanStack Router + React + + + +
+ + + +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite dev", + "preview": "vite preview" + }, + "devDependencies": { + "@tanstack/react-router": "^1.157.16", + "@tanstack/react-router-devtools": "^1.157.16", + "@tanstack/router-plugin": "^1.157.16", + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "nitro": "latest", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "vite": "beta" + } +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig", + "compilerOptions": { + "baseUrl": ".", + "jsx": "react-jsx", + "paths": { + "@/*": ["sec/*"] + } + } +} +``` + +```js [vite.config.mjs] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; +import react from "@vitejs/plugin-react"; +import { tanstackRouter } from "@tanstack/router-plugin/vite"; + +export default defineConfig({ + plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()], +}); +``` + +```tsx [src/main.tsx] +import { StrictMode } from "react"; +import ReactDOM from "react-dom/client"; +import { RouterProvider, createRouter } from "@tanstack/react-router"; + +// Import the generated route tree +import { routeTree } from "./routeTree.gen.ts"; + +// Create a new router instance +const router = createRouter({ routeTree }); + +// Register the router instance for type safety +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +// Render the app +const rootElement = document.querySelector("#root")!; +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement); + root.render( + + + + ); +} +``` + +```ts [src/routeTree.gen.ts] +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() +``` + +```css [src/assets/main.css] +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #ff2056; + text-decoration: inherit; +} +a:hover { + color: #ff637e; +} + +body { + margin: 0; + display: flex; + flex-direction: column; + place-items: center; + justify-content: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; + transition: transform 300ms; +} +.logo:hover { + transform: scale(1.1); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} +``` + +```tsx [src/routes/__root.tsx] +import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; + +const RootLayout = () => ( + <> +
+ + Home + +
+
+ + + +); + +export const Route = createRootRoute({ component: RootLayout }); +``` + +```tsx [src/routes/index.tsx] +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ + loader: async () => { + const r = await fetch("/api/hello"); + return r.json(); + }, + component: Index, +}); + +function Index() { + const r = Route.useLoaderData(); + + return ( +
+

{JSON.stringify(r)}

+
+ ); +} +``` + +:: + + + + + +Set up TanStack Router with React, Vite, and Nitro. This setup provides file-based routing with type-safe navigation and automatic code splitting. + +## Overview + +1. Add the Nitro Vite plugin to your Vite config +2. Create an HTML template with your app entry +3. Create a main entry that initializes the router +4. Define routes using file-based routing + +## 1. Configure Vite + +Add the Nitro, React, and TanStack Router plugins to your Vite config: + +```js [vite.config.mjs] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; +import react from "@vitejs/plugin-react"; +import { tanstackRouter } from "@tanstack/router-plugin/vite"; + +export default defineConfig({ + plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()], +}); +``` + +The `tanstackRouter` plugin generates a route tree from your `routes/` directory structure. Enable `autoCodeSplitting` to automatically split routes into separate chunks. Place the TanStack Router plugin before the React plugin in the array. + +## 2. Create the HTML Template + +Create an HTML file that serves as your app shell: + +```html [index.html] + + + + + + Nitro + TanStack Router + React + + + +
+ + + +``` + +## 3. Create the App Entry + +Create the main entry that initializes TanStack Router: + +```tsx [src/main.tsx] +import { StrictMode } from "react"; +import ReactDOM from "react-dom/client"; +import { RouterProvider, createRouter } from "@tanstack/react-router"; + +// Import the generated route tree +import { routeTree } from "./routeTree.gen.ts"; + +// Create a new router instance +const router = createRouter({ routeTree }); + +// Register the router instance for type safety +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +// Render the app +const rootElement = document.querySelector("#root")!; +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement); + root.render( + + + + ); +} +``` + +The `routeTree.gen.ts` file is auto-generated from your `routes/` directory structure. The `Register` interface declaration provides full type inference for route paths and params. The `!rootElement.innerHTML` check prevents re-rendering during hot module replacement. + +## 4. Create the Root Route + +The root route (`__root.tsx`) defines your app's layout: + +```tsx [src/routes/__root.tsx] +import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; + +const RootLayout = () => ( + <> +
+ + Home + +
+
+ + + +); + +export const Route = createRootRoute({ component: RootLayout }); +``` + +Use `Link` for type-safe navigation with active state styling. The `Outlet` component renders child routes. Include `TanStackRouterDevtools` for development tools (automatically removed in production). + +## 5. Create Page Routes + +Page routes use `createFileRoute` and can include loaders: + +```tsx [src/routes/index.tsx] +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ + loader: async () => { + const r = await fetch("/api/hello"); + return r.json(); + }, + component: Index, +}); + +function Index() { + const r = Route.useLoaderData(); + + return ( +
+

{JSON.stringify(r)}

+
+ ); +} +``` + +Fetch data before rendering with the `loader` function—data is available via `Route.useLoaderData()`. File paths determine URL paths: `routes/index.tsx` maps to `/`, `routes/about.tsx` to `/about`, and `routes/users/$id.tsx` to `/users/:id`. + + + +## Learn More + +- [TanStack Router Documentation](https://tanstack.com/router) +- [Renderer](/docs/renderer) diff --git a/examples/vite-ssr-tss-react/GUIDE.md b/examples/vite-ssr-tss-react/GUIDE.md new file mode 100644 index 0000000000..c3808957d4 --- /dev/null +++ b/examples/vite-ssr-tss-react/GUIDE.md @@ -0,0 +1,153 @@ +Set up TanStack Start with Nitro for a full-stack React framework experience with server-side rendering, file-based routing, and integrated API routes. + +## Overview + +1. Add the Nitro Vite plugin to your Vite config +2. Create a server entry using TanStack Start's server handler +3. Configure the router with default components +4. Define routes and API endpoints using file-based routing + +## 1. Configure Vite + +Add the Nitro, React, TanStack Start, and Tailwind plugins to your Vite config: + +```js [vite.config.mjs] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import viteReact from "@vitejs/plugin-react"; +import viteTsConfigPaths from "vite-tsconfig-paths"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [ + viteTsConfigPaths({ projects: ["./tsconfig.json"] }), + tanstackStart(), + viteReact(), + tailwindcss(), + nitro(), + ], + environments: { + ssr: { build: { rollupOptions: { input: "./server.ts" } } }, + }, +}); +``` + +The `tanstackStart()` plugin provides full SSR integration with automatic client entry handling. Use `viteTsConfigPaths()` to enable path aliases like `~/` from tsconfig. The `environments.ssr` option points to the server entry file. + +## 2. Create the Server Entry + +Create a server entry that uses TanStack Start's handler: + +```ts [server.ts] +import handler, { createServerEntry } from "@tanstack/react-start/server-entry"; + +export default createServerEntry({ + fetch(request) { + return handler.fetch(request); + }, +}); +``` + +TanStack Start handles SSR automatically. The `createServerEntry` wrapper integrates with Nitro's server entry format, and the `handler.fetch` processes all incoming requests. + +## 3. Configure the Router + +Create a router factory function with default error and not-found components: + +```tsx [src/router.tsx] +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen.ts"; + +export function getRouter() { + const router = createRouter({ + routeTree, + defaultPreload: "intent", + defaultErrorComponent: () =>
Internal Server Error
, + defaultNotFoundComponent: () =>
Not Found
, + scrollRestoration: true, + }); + return router; +} +``` + +The router factory configures preloading behavior, scroll restoration, and default error/not-found components. + +## 4. Create the Root Route + +The root route defines your HTML shell with head management and scripts: + +```tsx [src/routes/__root.tsx] +/// +import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import * as React from "react"; +import appCss from "~/styles/app.css?url"; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: "utf8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + ], + links: [{ rel: "stylesheet", href: appCss }], + scripts: [{ src: "/customScript.js", type: "text/javascript" }], + }), + errorComponent: () =>

500: Internal Server Error

, + notFoundComponent: () =>

404: Page Not Found

, + shellComponent: RootDocument, +}); + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+ + Home + {" "} + + 404 + +
+
+ {children} + + + + + ); +} +``` + +Define meta tags, stylesheets, and scripts in the `head()` function. The `shellComponent` provides the HTML document shell that wraps all pages. Use `HeadContent` to render the head configuration and `Scripts` to inject the client-side JavaScript for hydration. + +## 5. Create Page Routes + +Page routes define your application pages: + +```tsx [src/routes/index.tsx] +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ component: Home }); + +function Home() { + return ( +
+

Welcome Home!

+ /api/test +
+ ); +} +``` + +## API Routes + +TanStack Start supports API routes alongside page routes. Create files in `src/routes/api/` to define server endpoints that Nitro serves automatically. diff --git a/examples/vite-ssr-tss-react/README.md b/examples/vite-ssr-tss-react/README.md new file mode 100644 index 0000000000..7383f4ce55 --- /dev/null +++ b/examples/vite-ssr-tss-react/README.md @@ -0,0 +1,328 @@ +--- +category: server side rendering +icon: i-simple-icons-tanstack +--- + +# SSR with TanStack Start + +> Full-stack React with TanStack Start in Nitro using Vite. + + + +::code-tree{defaultValue="server.ts" expandAll} + +```text [.gitignore] +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +.nitro +/build/ +/api/ +/server/build +/public/build# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +.tanstack +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite dev", + "start": "node .output/server/index.mjs" + }, + "dependencies": { + "@tanstack/react-router": "^1.157.16", + "@tanstack/react-router-devtools": "^1.157.16", + "@tanstack/react-start": "^1.157.16", + "nitro": "latest", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "tailwind-merge": "^3.4.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.18", + "@types/node": "latest", + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + "vite": "beta", + "vite-tsconfig-paths": "^6.0.5" + } +} +``` + +```ts [server.ts] +import handler, { createServerEntry } from "@tanstack/react-start/server-entry"; + +export default createServerEntry({ + fetch(request) { + return handler.fetch(request); + }, +}); +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig", + "compilerOptions": { + "baseUrl": ".", + "jsx": "react-jsx", + "paths": { + "~/*": ["./src/*"] + } + } +} +``` + +```js [vite.config.mjs] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import viteReact from "@vitejs/plugin-react"; +import viteTsConfigPaths from "vite-tsconfig-paths"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [ + viteTsConfigPaths({ projects: ["./tsconfig.json"] }), + tanstackStart(), + viteReact(), + tailwindcss(), + nitro(), + ], + environments: { + ssr: { build: { rollupOptions: { input: "./server.ts" } } }, + }, +}); +``` + +```tsx [src/router.tsx] +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen.ts"; + +export function getRouter() { + const router = createRouter({ + routeTree, + defaultPreload: "intent", + defaultErrorComponent: () =>
Internal Server Error
, + defaultNotFoundComponent: () =>
Not Found
, + scrollRestoration: true, + }); + return router; +} +``` + +```ts [src/routeTree.gen.ts] +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiTestRouteImport } from './routes/api/test' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiTestRoute = ApiTestRouteImport.update({ + id: '/api/test', + path: '/api/test', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/test': typeof ApiTestRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/test': typeof ApiTestRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/test': typeof ApiTestRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/test' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/test' + id: '__root__' | '/' | '/api/test' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiTestRoute: typeof ApiTestRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/test': { + id: '/api/test' + path: '/api/test' + fullPath: '/api/test' + preLoaderRoute: typeof ApiTestRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiTestRoute: ApiTestRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} +``` + +```tsx [src/routes/__root.tsx] +/// +import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import * as React from "react"; +import appCss from "~/styles/app.css?url"; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: "utf8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + ], + links: [{ rel: "stylesheet", href: appCss }], + scripts: [{ src: "/customScript.js", type: "text/javascript" }], + }), + errorComponent: () =>

500: Internal Server Error

, + notFoundComponent: () =>

404: Page Not Found

, + shellComponent: RootDocument, +}); + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+ + Home + {" "} + + 404 + +
+
+ {children} + + + + + ); +} +``` + +```tsx [src/routes/index.tsx] +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ component: Home }); + +function Home() { + return ( +
+

Welcome Home!

+ /api/test +
+ ); +} +``` + +```css [src/styles/app.css] +@import "tailwindcss"; + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} +``` + +:: + + + + + + + +## Learn More + +- [TanStack Start Documentation](https://tanstack.com/start) +- [Server Entry](/docs/server-entry) diff --git a/examples/vite-ssr-vue-router/GUIDE.md b/examples/vite-ssr-vue-router/GUIDE.md new file mode 100644 index 0000000000..b4ad7ac154 --- /dev/null +++ b/examples/vite-ssr-vue-router/GUIDE.md @@ -0,0 +1,241 @@ +Set up server-side rendering (SSR) with Vue, Vue Router, Vite, and Nitro. This setup enables per-route code splitting, head management with unhead, and client hydration. + +## Overview + +1. Add the Nitro Vite plugin to your Vite config +2. Define routes with lazy-loaded components +3. Create a server entry that renders your app with router support +4. Create a client entry that hydrates and takes over routing +5. Create page components + +## 1. Configure Vite + +Add the Nitro and Vue plugins to your Vite config. Define both `client` and `ssr` environments: + +```js [vite.config.mjs] +import vue from "@vitejs/plugin-vue"; +import { defineConfig } from "vite"; +import devtoolsJson from "vite-plugin-devtools-json"; +import { nitro } from "nitro/vite"; + +export default defineConfig((_env) => ({ + plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], + environments: { + client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, + ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, + }, +})); + +// Workaround https://github.com/vitejs/vite-plugin-vue/issues/677 +function patchVueExclude(plugin, exclude) { + const original = plugin.transform.handler; + plugin.transform.handler = function (...args) { + if (exclude.test(args[1])) return; + return original.call(this, ...args); + }; + return plugin; +} +``` + +The `patchVueExclude` helper prevents the Vue plugin from processing asset imports (files with `?assets` query parameter). + +## 2. Define Routes + +Create route definitions with lazy-loaded components and asset metadata: + +```ts [app/routes.ts] +import type { RouteRecordRaw } from "vue-router"; + +export const routes: RouteRecordRaw[] = [ + { + path: "/", + name: "app", + component: () => import("./app.vue"), + meta: { + assets: () => import("./app.vue?assets"), + }, + children: [ + { + path: "/", + name: "home", + component: () => import("./pages/index.vue"), + meta: { + assets: () => import("./pages/index.vue?assets"), + }, + }, + { + path: "/about", + name: "about", + component: () => import("./pages/about.vue"), + meta: { + assets: () => import("./pages/about.vue?assets"), + }, + }, + { + path: "/:catchAll(.*)", + name: "not-found", + component: () => import("./pages/not-found.vue"), + meta: { + assets: () => import("./pages/not-found.vue?assets"), + }, + }, + ], + }, +]; +``` + +Use dynamic imports for lazy-loaded components to enable code splitting. The `meta.assets` function loads route-specific CSS and JS chunks. Define child routes under a root layout component for nested routing. + +## 3. Create the Server Entry + +The server entry renders your Vue app with router support and head management: + +```ts [app/entry-server.ts] +import { createSSRApp } from "vue"; +import { renderToString } from "vue/server-renderer"; +import { RouterView, createMemoryHistory, createRouter } from "vue-router"; +import { createHead, transformHtmlTemplate } from "unhead/server"; + +import { routes } from "./routes.ts"; + +import clientAssets from "./entry-client.ts?assets=client"; + +async function handler(request: Request): Promise { + const app = createSSRApp(RouterView); + const router = createRouter({ history: createMemoryHistory(), routes }); + app.use(router); + + const url = new URL(request.url); + const href = url.href.slice(url.origin.length); + + await router.push(href); + await router.isReady(); + + const assets = clientAssets.merge( + ...(await Promise.all( + router.currentRoute.value.matched + .map((to) => to.meta.assets) + .filter(Boolean) + .map((fn) => (fn as any)().then((m: any) => m.default)) + )) + ); + + const head = createHead(); + + head.push({ + link: [ + ...assets.css.map((attrs: any) => ({ rel: "stylesheet", ...attrs })), + ...assets.js.map((attrs: any) => ({ rel: "modulepreload", ...attrs })), + ], + script: [{ type: "module", src: clientAssets.entry }], + }); + + const renderedApp = await renderToString(app); + + const html = await transformHtmlTemplate(head, htmlTemplate(renderedApp)); + + return new Response(html, { + headers: { "Content-Type": "text/html;charset=utf-8" }, + }); +} + +function htmlTemplate(body: string): string { + return /* html */ ` + + + + + Vue Router Custom Framework + + +
${body}
+ +`; +} + +export default { + fetch: handler, +}; +``` + +The server uses `createMemoryHistory()` since there's no browser URL bar—the router navigates to the requested URL before rendering. Assets are loaded dynamically based on matched routes, ensuring only the CSS and JS needed for the current page are included. The `unhead` library manages `` elements, injecting stylesheets and scripts via `transformHtmlTemplate`. + +## 4. Create the Client Entry + +The client entry hydrates the server-rendered HTML and takes over routing: + +```ts [app/entry-client.ts] +import { createSSRApp } from "vue"; +import { RouterView, createRouter, createWebHistory } from "vue-router"; +import { routes } from "./routes.ts"; + +async function main() { + const app = createSSRApp(RouterView); + const router = createRouter({ history: createWebHistory(), routes }); + app.use(router); + + await router.isReady(); + app.mount("#root"); +} + +// eslint-disable-next-line unicorn/prefer-top-level-await +main(); +``` + +The client entry creates a Vue app with `createWebHistory()` for browser-based routing. After the router is ready, it mounts to the `#root` element and hydrates the server-rendered HTML. + +## 5. Create the Root Component + +The root component provides navigation and renders child routes: + +```vue [app/app.vue] + + + + + +``` diff --git a/examples/vite-ssr-vue-router/README.md b/examples/vite-ssr-vue-router/README.md new file mode 100644 index 0000000000..59875a9771 --- /dev/null +++ b/examples/vite-ssr-vue-router/README.md @@ -0,0 +1,389 @@ +--- +category: server side rendering +icon: i-logos-vue +--- + +# SSR with Vue Router + +> Server-side rendering with Vue Router in Nitro using Vite. + + + +::code-tree{defaultValue="app/entry-server.ts" expandAll} + +```json [package.json] +{ + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite dev", + "preview": "vite preview" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.3", + "nitro": "latest", + "unhead": "^2.1.2", + "vite": "beta", + "vite-plugin-devtools-json": "^1.0.0", + "vue": "^3.5.27", + "vue-router": "^4.6.4" + } +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```js [vite.config.mjs] +import vue from "@vitejs/plugin-vue"; +import { defineConfig } from "vite"; +import devtoolsJson from "vite-plugin-devtools-json"; +import { nitro } from "nitro/vite"; + +export default defineConfig((_env) => ({ + plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], + environments: { + client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, + ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, + }, +})); + +// Workaround https://github.com/vitejs/vite-plugin-vue/issues/677 +function patchVueExclude(plugin, exclude) { + const original = plugin.transform.handler; + plugin.transform.handler = function (...args) { + if (exclude.test(args[1])) return; + return original.call(this, ...args); + }; + return plugin; +} +``` + +```vue [app/app.vue] + + + + + +``` + +```ts [app/entry-client.ts] +import { createSSRApp } from "vue"; +import { RouterView, createRouter, createWebHistory } from "vue-router"; +import { routes } from "./routes.ts"; + +async function main() { + const app = createSSRApp(RouterView); + const router = createRouter({ history: createWebHistory(), routes }); + app.use(router); + + await router.isReady(); + app.mount("#root"); +} + +// eslint-disable-next-line unicorn/prefer-top-level-await +main(); +``` + +```ts [app/entry-server.ts] +import { createSSRApp } from "vue"; +import { renderToString } from "vue/server-renderer"; +import { RouterView, createMemoryHistory, createRouter } from "vue-router"; +import { createHead, transformHtmlTemplate } from "unhead/server"; + +import { routes } from "./routes.ts"; + +import clientAssets from "./entry-client.ts?assets=client"; + +async function handler(request: Request): Promise { + const app = createSSRApp(RouterView); + const router = createRouter({ history: createMemoryHistory(), routes }); + app.use(router); + + const url = new URL(request.url); + const href = url.href.slice(url.origin.length); + + await router.push(href); + await router.isReady(); + + const assets = clientAssets.merge( + ...(await Promise.all( + router.currentRoute.value.matched + .map((to) => to.meta.assets) + .filter(Boolean) + .map((fn) => (fn as any)().then((m: any) => m.default)) + )) + ); + + const head = createHead(); + + head.push({ + link: [ + ...assets.css.map((attrs: any) => ({ rel: "stylesheet", ...attrs })), + ...assets.js.map((attrs: any) => ({ rel: "modulepreload", ...attrs })), + ], + script: [{ type: "module", src: clientAssets.entry }], + }); + + const renderedApp = await renderToString(app); + + const html = await transformHtmlTemplate(head, htmlTemplate(renderedApp)); + + return new Response(html, { + headers: { "Content-Type": "text/html;charset=utf-8" }, + }); +} + +function htmlTemplate(body: string): string { + return /* html */ ` + + + + + Vue Router Custom Framework + + +
${body}
+ +`; +} + +export default { + fetch: handler, +}; +``` + +```ts [app/routes.ts] +import type { RouteRecordRaw } from "vue-router"; + +export const routes: RouteRecordRaw[] = [ + { + path: "/", + name: "app", + component: () => import("./app.vue"), + meta: { + assets: () => import("./app.vue?assets"), + }, + children: [ + { + path: "/", + name: "home", + component: () => import("./pages/index.vue"), + meta: { + assets: () => import("./pages/index.vue?assets"), + }, + }, + { + path: "/about", + name: "about", + component: () => import("./pages/about.vue"), + meta: { + assets: () => import("./pages/about.vue?assets"), + }, + }, + { + path: "/:catchAll(.*)", + name: "not-found", + component: () => import("./pages/not-found.vue"), + meta: { + assets: () => import("./pages/not-found.vue?assets"), + }, + }, + ], + }, +]; +``` + +```ts [app/shims.d.ts] +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} +``` + +```css [app/styles.css] +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: #f5f5f5; + color: #333; +} + +main { + max-width: 800px; + margin: 0 auto; + padding: 2rem; +} + +h1 { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.card { + background: white; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin: 2rem 0; +} + +button { + background: rgb(83, 91, 242); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + font-size: 1rem; + cursor: pointer; +} + +button:hover { + background: #535bf2; +} + +.subtitle { + color: #666; + font-size: 1.1rem; + margin-bottom: 2rem; +} +``` + +```vue [app/pages/about.vue] + +``` + +```vue [app/pages/index.vue] + + + + + +``` + +```vue [app/pages/not-found.vue] + +``` + +:: + + + + + + + +## Learn More + +- [Vue Router Documentation](https://router.vuejs.org/) +- [Unhead Documentation](https://unhead.unjs.io/) +- [Renderer](/docs/renderer) +- [Server Entry](/docs/server-entry) diff --git a/examples/vite-trpc/GUIDE.md b/examples/vite-trpc/GUIDE.md new file mode 100644 index 0000000000..87d2f9075f --- /dev/null +++ b/examples/vite-trpc/GUIDE.md @@ -0,0 +1,162 @@ +Set up tRPC with Vite and Nitro for end-to-end typesafe APIs without code generation. This example builds a counter with server-side rendering for the initial value and client-side updates. + +## Overview + +1. Configure Vite with the Nitro plugin and route tRPC requests +2. Create a tRPC router with procedures +3. Create an HTML page with server-side rendering and client interactivity + +## 1. Configure Vite + +Add the Nitro plugin and configure the `/trpc/**` route to point to your tRPC handler: + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ + plugins: [ + nitro({ + routes: { + "/trpc/**": "./server/trpc.ts", + }, + }), + ], +}); +``` + +The `routes` option maps URL patterns to handler files. All requests to `/trpc/*` are handled by the tRPC router. + +## 2. Create the tRPC Router + +Define your tRPC router with procedures and export it as a fetch handler: + +```ts [server/trpc.ts] +import { initTRPC } from "@trpc/server"; +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; + +let counter = 0; + +const t = initTRPC.create(); + +export const appRouter = t.router({ + get: t.procedure.query(() => { + return { value: counter }; + }), + + inc: t.procedure.mutation(() => { + counter++; + return { value: counter }; + }), +}); + +export type AppRouter = typeof appRouter; + +export default { + async fetch(request: Request): Promise { + return fetchRequestHandler({ + endpoint: "/trpc", + req: request, + router: appRouter, + }); + }, +}; +``` + +Define procedures using `t.procedure.query()` for read operations and `t.procedure.mutation()` for write operations. Export the `AppRouter` type so clients get full type inference. The default export uses tRPC's fetch adapter to handle incoming requests. + +## 3. Create the HTML Page + +Create an HTML page with server-side rendering and client-side interactivity: + +```html [index.html] + + + + + tRPC Counter + + + +
+
Counter
+
+ +
+ +
+ + + + +``` + +The ` + + + + + + + +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@trpc/client": "^11.8.1", + "@trpc/server": "^11.8.1", + "nitro": "latest", + "vite": "beta", + "zod": "^4.3.6" + } +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig", + "compilerOptions": {} +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ + plugins: [ + nitro({ + routes: { + "/trpc/**": "./server/trpc.ts", + }, + }), + ], +}); +``` + +```ts [server/trpc.ts] +import { initTRPC } from "@trpc/server"; +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; + +let counter = 0; + +const t = initTRPC.create(); + +export const appRouter = t.router({ + get: t.procedure.query(() => { + return { value: counter }; + }), + + inc: t.procedure.mutation(() => { + counter++; + return { value: counter }; + }), +}); + +export type AppRouter = typeof appRouter; + +export default { + async fetch(request: Request): Promise { + return fetchRequestHandler({ + endpoint: "/trpc", + req: request, + router: appRouter, + }); + }, +}; +``` + +:: + + + + + +Set up tRPC with Vite and Nitro for end-to-end typesafe APIs without code generation. This example builds a counter with server-side rendering for the initial value and client-side updates. + +## Overview + +1. Configure Vite with the Nitro plugin and route tRPC requests +2. Create a tRPC router with procedures +3. Create an HTML page with server-side rendering and client interactivity + +## 1. Configure Vite + +Add the Nitro plugin and configure the `/trpc/**` route to point to your tRPC handler: + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ + plugins: [ + nitro({ + routes: { + "/trpc/**": "./server/trpc.ts", + }, + }), + ], +}); +``` + +The `routes` option maps URL patterns to handler files. All requests to `/trpc/*` are handled by the tRPC router. + +## 2. Create the tRPC Router + +Define your tRPC router with procedures and export it as a fetch handler: + +```ts [server/trpc.ts] +import { initTRPC } from "@trpc/server"; +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; + +let counter = 0; + +const t = initTRPC.create(); + +export const appRouter = t.router({ + get: t.procedure.query(() => { + return { value: counter }; + }), + + inc: t.procedure.mutation(() => { + counter++; + return { value: counter }; + }), +}); + +export type AppRouter = typeof appRouter; + +export default { + async fetch(request: Request): Promise { + return fetchRequestHandler({ + endpoint: "/trpc", + req: request, + router: appRouter, + }); + }, +}; +``` + +Define procedures using `t.procedure.query()` for read operations and `t.procedure.mutation()` for write operations. Export the `AppRouter` type so clients get full type inference. The default export uses tRPC's fetch adapter to handle incoming requests. + +## 3. Create the HTML Page + +Create an HTML page with server-side rendering and client-side interactivity: + +```html [index.html] + + + + + tRPC Counter + + + +
+
Counter
+
+ +
+ +
+ + + + +``` + +The ` + + + + +
+ +
+
+
+

{{ message.user }}

+
+ Avatar +
+

+

{{ message.text }}

+
+
+

{{ message.date }}

+
+
+
+ + +
+
+ +
+
+ + + + +
+
+
+ + +` +``` + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + serverDir: "./", + renderer: { static: true }, + features: { websocket: true }, +}); +``` + +```json [package.json] +{ + "type": "module", + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + "devDependencies": { + "nitro": "latest" + } +} +``` + +```json [tsconfig.json] +{ + "extends": "nitro/tsconfig" +} +``` + +```ts [vite.config.ts] +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ plugins: [nitro()] }); +``` + +```ts [routes/_ws.ts] +import { defineWebSocketHandler } from "nitro/h3"; + +export default defineWebSocketHandler({ + open(peer) { + peer.send({ user: "server", message: `Welcome ${peer}!` }); + peer.publish("chat", { user: "server", message: `${peer} joined!` }); + peer.subscribe("chat"); + }, + message(peer, message) { + if (message.text().includes("ping")) { + peer.send({ user: "server", message: "pong" }); + } else { + const msg = { + user: peer.toString(), + message: message.toString(), + }; + peer.send(msg); // echo + peer.publish("chat", msg); + } + }, + close(peer) { + peer.publish("chat", { user: "server", message: `${peer} left!` }); + }, +}); +``` + +:: + + + + + +This example implements a simple chat room using WebSockets. Clients connect, send messages, and receive messages from other users in real-time. The server broadcasts messages to all connected clients using pub/sub channels. + +## WebSocket Handler + +Create a WebSocket route using `defineWebSocketHandler`. + +```ts [routes/_ws.ts] +import { defineWebSocketHandler } from "nitro/h3"; + +export default defineWebSocketHandler({ + open(peer) { + peer.send({ user: "server", message: `Welcome ${peer}!` }); + peer.publish("chat", { user: "server", message: `${peer} joined!` }); + peer.subscribe("chat"); + }, + message(peer, message) { + if (message.text().includes("ping")) { + peer.send({ user: "server", message: "pong" }); + } else { + const msg = { + user: peer.toString(), + message: message.toString(), + }; + peer.send(msg); // echo + peer.publish("chat", msg); + } + }, + close(peer) { + peer.publish("chat", { user: "server", message: `${peer} left!` }); + }, +}); +``` + +Different hooks are exposed by `defineWebSocketHandler()` to integrate with different parts of the websocket lifecycle. + + + +## Learn More + +- [Routing](/docs/routing) +- [crossws Documentation](https://crossws.h3.dev/guide/hooks)