Build applications with @lazarv/react-server — a React Server Components runtime built on Vite. Covers use directives, file-system router, HTTP hooks, caching, live components, workers, MCP, deployment, and all core APIs.
You are working on a project that uses @lazarv/react-server — an open React Server Components runtime (not a framework). It is built on the Vite Environment API and supports Node.js, Bun, and Deno.
Full documentation: https://react-server.dev Any docs page as markdown: https://react-server.dev/{path}.md JSON Schema for configuration: https://react-server.dev/schema.json Examples: https://github.com/lazarv/react-server/tree/main/examples
When you need more detail on a specific topic, fetch the relevant markdown page from the docs site.
# Development (with explicit entrypoint)
pnpm react-server ./App.jsx
# Development (file-system router, no entrypoint needed)
pnpm react-server
# Production build
pnpm react-server build [./App.jsx]
# Start production server
pnpm react-server start
# Useful flags
# --open Open browser on start
# --host 0.0.0.0 Listen on all interfaces
# --port 3001 Custom port
# --https Enable HTTPS
These directives go at the top of a file or inside a function body (lexically scoped RSC):
"use client" — Client component (enables React hooks, event handlers, browser APIs)"use server" — Server function (callable from client, receives FormData as first arg)"use live" — Live component (async generator that yields JSX, streams updates via WebSocket)"use worker" — Worker module (offloads to Worker Thread on server, Web Worker on client)"use cache" — Cached function with options: "use cache; ttl=200; tags=todos" or "use cache; profile=todos" or "use cache: file; tags=todos" or "use cache: request" (per-request dedup) or "use cache: request; no-hydrate" (no browser hydration)"use dynamic" — Force dynamic rendering (opt out of static/prerender)"use static" — Force static rendering at build timeWhen no entrypoint is passed to the CLI, the file-system router is used. Default root is src/pages (configurable via root in config).
src/pages/
├── layout.jsx # Root layout (wraps all routes)
├── page.jsx # Index page (/)
├── about.jsx # /about (any .jsx/.tsx file is a page)
├── (group)/ # Route group (no URL segment)
│ └── page.jsx
├── users/
│ ├── page.jsx # /users
│ ├── [id].page.jsx # /users/:id (dynamic segment)
│ └── [id]/
│ └── posts.page.jsx # /users/:id/posts
├── docs/
│ └── [...slug].page.jsx # /docs/* (catch-all, slug is string[])
├── @sidebar/ # Parallel route / outlet named "sidebar"
│ └── page.jsx
├── loading.page.jsx # Loading fallback (Suspense boundary)
├── error.page.jsx # Error fallback (ErrorBoundary)
├── index.middleware.mjs # Middleware for this route segment
├── GET.posts.server.mjs # API route: GET /posts
└── mcp.server.mjs # MCP endpoint at /mcp
Key conventions:
page.jsx or index.jsx — route page (index route)"use client" page — client-only route (no server round-trip, state preserved between navigations)layout.jsx — wraps child routes with persistent layout[param] — dynamic route parameter (passed as prop)[...slug] — catch-all (param is string[])[[...slug]] — optional catch-all(name) — route group / transparent segment (not in URL)@name/ — parallel route outlet*.middleware.{js,mjs,ts,mts} — middleware*.server.{js,mjs,ts,mts} — API route handlerGET.*.server.mjs / POST.*.server.mjs — HTTP method-specific API route{filename.xml}.server.mjs — escaped route segment for special characters*.resource.{js,mjs,ts,mts} — resource file (auto-bound to route)Route validation (export validate object with params/search schemas from any page):
import { z } from "zod";
import { user } from "@lazarv/react-server/routes";
export const validate = {
params: z.object({ id: z.string().regex(/^\d+$/) }),
};
export default user.createPage(({ id }) => <h1>User {id}</h1>);
Auto-generated routes module (@lazarv/react-server/routes): exports named route descriptors for every route. Each has .Link, .href(), .useParams(), .useSearchParams(), .createPage(), .createLayout(), .createLoading(), .createError(), .createMiddleware().
// Core hooks and utilities
import {
useHttpContext,
useUrl,
usePathname,
useSearchParams,
useRequest,
useResponse,
useFormData,
headers,
cookie,
setCookie,
deleteCookie,
status,
redirect,
rewrite,
after,
useCache,
useResponseCache,
withCache,
revalidate,
invalidate,
getRuntime,
} from "@lazarv/react-server";
// Configuration helper
import { defineConfig } from "@lazarv/react-server/config";
// Navigation (client-side)
import { Link, Refresh, ReactServerComponent, ScrollRestoration, useNavigate } from "@lazarv/react-server/navigation";
import { useClient } from "@lazarv/react-server/navigation";
// useClient() returns { navigate, replace, prefetch }
// Typed router
import { createRoute, createRouter, Route, SearchParams } from "@lazarv/react-server/router";
// Resources (typed, schema-validated data fetching)
import { createResource, createResources, resources } from "@lazarv/react-server/resources";
// Auto-generated routes module (file-system router only)
import { index, about, user } from "@lazarv/react-server/routes";
// Error handling
import { ErrorBoundary } from "@lazarv/react-server/error-boundary";
// Client utilities
import { ClientOnly } from "@lazarv/react-server/client";
// Micro-frontends
import RemoteComponent from "@lazarv/react-server/remote";
// Workers
import { isWorker } from "@lazarv/react-server/worker";
// MCP (Model Context Protocol)
import { createServer, createTool, createResource as createMcpResource, createPrompt } from "@lazarv/react-server/mcp";
@lazarv/react-server includes a fully typed routing solution with compile-time type safety for route paths, params, and search params. No code generation step — works with any schema library (Zod, ArkType, Valibot) or lightweight parse functions.
import { createRoute } from "@lazarv/react-server/router";
import { z } from "zod";
// Static route
export const home = createRoute("/", { exact: true });
// Dynamic route — params extracted from pattern
export const user = createRoute("/user/[id]", {
exact: true,
validate: { params: z.object({ id: z.coerce.number().int().positive() }) },
});
// Search params with validation
export const products = createRoute("/products", {
exact: true,
validate: {
search: z.object({
sort: z.enum(["name", "price", "rating"]).catch("name"),
page: z.coerce.number().int().positive().catch(1),
}),
},
});
// Lightweight parse (no schema library)
export const post = createRoute("/post/[slug]", {
exact: true,
parse: { params: { slug: String }, search: { tab: String } },
});
// Fallback routes
export const notFound = createRoute("*");
export const userNotFound = createRoute("/user/*"); // scoped fallback
import { createRoute, createRouter } from "@lazarv/react-server/router";
import * as routes from "./routes";
const router = createRouter({
home: createRoute(routes.home, <Home />),
user: createRoute(routes.user, <UserPage />),
notFound: createRoute(routes.notFound, <NotFound />),
});
export default function App() {
return (
<div>
<nav>
<router.home.Link>Home</router.home.Link>
<router.user.Link params={{ id: 42 }}>User 42</router.user.Link>
</nav>
<router.Routes />
</div>
);
}
Routes without server components — useful for tabs, modals, filters. Define with createRoute (no element) and use in "use client" components:
"use client";
import { settings } from "./routes";
export default function SettingsPage() {
const params = settings.useParams();
return (
<nav>
<settings.Link params={{ tab: "profile" }}>Profile</settings.Link>
<settings.Link params={{ tab: "security" }}>Security</settings.Link>
</nav>
);
}
In the file-system router, any page with "use client" at the top is automatically a client-only route.
Every route descriptor provides: .Link (typed Link component), .href(params?) (URL builder), .useParams() (typed params or null), .useSearchParams() (typed search params), .SearchParams (route-scoped transform boundary).
<products.Link search={(prev) => ({ ...prev, page: prev.page + 1 })}>
Next Page
</products.Link>
Bidirectional URL ↔ app search param transforms with decode and encode:
<products.SearchParams decode={decode} encode={encode}>
{children}
</products.SearchParams>
import { useNavigate } from "@lazarv/react-server/navigation";
const navigate = useNavigate();
navigate(user, { params: { id: 42 } });
Schema-validated, route-aware data fetching with "use cache" as the caching runtime.
import { createResource } from "@lazarv/react-server/resources";
import { z } from "zod";
// Define + bind loader
export const userById = createResource({
key: z.object({ id: z.coerce.number().int().positive() }),
}).bind(async ({ id }) => {
"use cache";
return db.users.findById(id);
});
// Singleton (no key)
export const currentUser = createResource().bind(async () => {
return (await getSession()).user;
});
Using in components:
// .use() — suspense-integrated, auto-re-renders on invalidation
const userData = userById.use({ id });
// .query() — imperative, returns Promise
const user = await userById.query({ id: 42 });
// .prefetch() — warm cache
userById.prefetch({ id: 42 });
Route-resource bindings for automatic pre-loading:
const todosMapping = todos.from(({ search }) => ({ filter: search.filter ?? "all" }));
// Pass to createRoute: createRoute(routes.todos, <TodosPage />, { resources: [todosMapping] })
Enable via config or component:
// react-server.config.mjs
export default { scrollRestoration: true };
// Or with options:
export default { scrollRestoration: { behavior: "smooth" } };
import { ScrollRestoration } from "@lazarv/react-server/navigation";
<ScrollRestoration behavior="smooth" />
Handles: forward navigation (scroll to top or #hash), back/forward (restore saved position), page refresh (zero-flash restore), query-only changes (preserve position), nested scroll containers, prefers-reduced-motion support.
// Server components can be async and access backend resources directly
export default async function Page() {
const data = await db.query("SELECT * FROM posts");
return <ul>{data.map(post => <li key={post.id}>{post.title}</li>)}</ul>;
}
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
// File-level directive
"use server";
export async function createPost(formData) {
const title = formData.get("title");
await db.insert({ title });
}
// Or inline in a server component
export default function Page() {
async function handleSubmit(formData) {
"use server";
await db.insert({ title: formData.get("title") });
}
return (
<form action={handleSubmit}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}
"use live";
export default async function* LiveClock() {
while (true) {
yield <div>{new Date().toLocaleTimeString()}</div>;
await new Promise(r => setTimeout(r, 1000));
}
}
"use worker";
// All exports must be async functions
export async function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// No file-level directive — this is a server component
export default async function Page() {
const data = await fetchData();
// Inline client component — extracted at build time, no separate file needed
function InteractiveList({ items }) {
"use client";
const [filter, setFilter] = useState("");
return (
<>
<input value={filter} onChange={e => setFilter(e.target.value)} />
<ul>{items.filter(i => i.includes(filter)).map(i => <li key={i}>{i}</li>)}</ul>
</>
);
}
return <InteractiveList items={data} />;
}
Server → Client → Server nesting is also supported:
async function getData() {
"use server";
function Display({ value }) {
"use client";
const [show, setShow] = useState(false);
return <button onClick={() => setShow(!show)}>{show ? value : "Reveal"}</button>;
}
const value = await db.getSecret();
return <Display value={value} />;
}
Available in server components, middlewares, and API routes:
import {
useUrl, // URL object
usePathname, // string
useSearchParams, // URLSearchParams
useRequest, // Request object
useResponse, // Response object
useFormData, // FormData (POST requests)
useHttpContext, // full context object
headers, // headers(name?) to read, headers({ key: value }) to set
cookie, // cookie(name?) to read, cookie(name, value, opts) to set
setCookie, // setCookie(name, value, opts)
deleteCookie, // deleteCookie(name)
status, // status(code, statusText?)
redirect, // redirect(url, statusCode?)
rewrite, // rewrite(url)
after, // after(callback) — runs after response is sent
} from "@lazarv/react-server";
import { useResponseCache, useCache, withCache, revalidate, invalidate } from "@lazarv/react-server";
// Cache entire HTTP response for 30 seconds
useResponseCache(30_000);
// HOC form
export default withCache(async function Page() { ... }, 30_000);
// In-memory cache with compound key
const data = await useCache(
["posts", category],
() => db.query("SELECT * FROM posts WHERE category = ?", [category]),
60_000
);
// Invalidate
revalidate(["posts", category]);
Using the cache directive:
"use cache; ttl=30000; tags=posts";
export async function getPosts() {
return db.query("SELECT * FROM posts");
}
// Invalidate by tag
import { invalidate } from "@lazarv/react-server";
await invalidate(getPosts);
memory — default in-memory cacherequest — per-request deduplication, shared across RSC and SSR (see below)null — no-op cache (useful with aliases to disable caching)local — browser localStoragesession — browser sessionStorageThe request provider deduplicates calls within a single HTTP request. The function body runs once — all subsequent calls (including across RSC and SSR environments) return the cached result. Values are automatically dehydrated into the HTML and rehydrated in the browser during React hydration.
async function getPageData() {
"use cache: request";
return await fetchExpensiveData();
}
// Server component — awaits the cached value
async function ServerPart() {
const data = await getPageData();
return <div>{data.title}</div>;
}
// Client component — uses React's use() with the cached value
"use client";
import { use } from "react";
function ClientPart() {
const data = use(getPageData());
return <div>{data.title}</div>;
}
Disable hydration (don't embed value in HTML) with either syntax:
"use cache: request; hydrate=false";
// or equivalently:
"use cache: request; no-hydrate";
When hydrate=false / no-hydrate is set, SSR deduplication still works but the value is not embedded in the HTML — the client component recomputes it in the browser.
// react-server.config.mjs
import { defineConfig } from "@lazarv/react-server/config";
export default defineConfig({
root: "src/pages", // File router root directory
public: "public", // Static assets directory
port: 3000, // Server port
adapter: "vercel", // Deployment adapter
// adapter: ["cloudflare", { serverlessFunctions: false }],
scrollRestoration: true, // Enable scroll restoration (or { behavior: "smooth" })
mdx: { // MDX support
remarkPlugins: [],
rehypePlugins: [],
components: "./src/mdx-components.jsx",
},
cache: {
profiles: {
short: { ttl: 60_000 },
},
},
telemetry: { // OpenTelemetry
enabled: true,
serviceName: "my-app",
},
});
JSON config with schema validation:
{
"$schema": "https://react-server.dev/schema.json",
"port": 3000,
"adapter": "vercel"
}
Extension configs are merged: +tailwind.config.mjs, +auth.config.mjs, etc.
Mode-specific: .production.config.mjs, .development.config.mjs, .build.config.mjs.
Env variables: VITE_* and REACT_SERVER_* prefixed vars are exposed via import.meta.env.
import { Link, Refresh, ReactServerComponent, ScrollRestoration, useNavigate } from "@lazarv/react-server/navigation";
// Client-side navigation
<Link href="/about" prefetch>About</Link>
// Typed Link from route descriptor (compile-time param checking)
<user.Link params={{ id: 42 }}>User 42</user.Link>
// Link with search param updater
<products.Link search={(prev) => ({ ...prev, page: prev.page + 1 })}>Next</products.Link>
// Re-render current route
<Refresh>Reload</Refresh>
// Render an outlet
<ReactServerComponent outlet="sidebar">{sidebar}</ReactServerComponent>
// Scroll restoration
<ScrollRestoration behavior="smooth" />
// Programmatic navigation (client component only)
import { useClient } from "@lazarv/react-server/navigation";
const { navigate, replace, prefetch } = useClient();
// Typed programmatic navigation
const nav = useNavigate();
nav(user, { params: { id: 42 } });
import { ErrorBoundary } from "@lazarv/react-server/error-boundary";
<ErrorBoundary
fallback={<p>Loading...</p>}
component={({ error, resetErrorBoundary }) => (
<div>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<Content />
</ErrorBoundary>
File router conventions: error.page.jsx for error boundaries, loading.page.jsx for Suspense fallbacks.
// index.middleware.mjs
import { redirect, rewrite, usePathname, useMatch } from "@lazarv/react-server";
export default function Middleware() {
if (useMatch("/old-path")) {
redirect("/new-path");
}
}
// GET.posts.server.mjs
import { useSearchParams } from "@lazarv/react-server";
export default function GetPosts() {
const params = useSearchParams();
return Response.json({ posts: [], page: params.get("page") });
}
Available adapters: vercel, netlify, cloudflare, aws, azure, azure-swa, bun, deno, docker, firebase, singlefile.
// react-server.config.mjs
export default { adapter: "vercel" };
// Or with options:
export default { adapter: ["cloudflare", { serverlessFunctions: false }] };
import RemoteComponent from "@lazarv/react-server/remote";
export default function App() {
return (
<RemoteComponent
src="http://localhost:3001"
defer // streaming
isolate // Shadow DOM isolation
message="Hello"
/>
);
}
// mcp.server.mjs
import { createServer, createTool } from "@lazarv/react-server/mcp";
import { z } from "zod";
export default createServer({
tools: {
search: createTool({
id: "search",
title: "Search",
description: "Search the database",
inputSchema: { query: z.string() },
async handler({ query }) {
return await db.search(query);
},
}),
},
});