API Reference
Decorators
@Render(component, options?)
@Render(ProductDetail)
@Render(ProductDetail, { layout: MainLayout })
@Render(ProductDetail, { layout: MainLayout, layoutProps: { nav: true } })
@Render(ProductDetail, { layout: false }) // skip controller layout, keep root
@Render(ProductDetail, { layout: null }) // skip all layouts
@Render(ProductDetail, { representation: { json: true } })| Option | Type | Description |
|---|---|---|
layout | LayoutComponent | false | null | Layout override — see below |
layoutProps | object | Props for layout |
representation | RepresentationPolicy | Route representation policy — see below |
jsonApi | boolean | Deprecated — use representation.json instead |
layout semantics: a component replaces the controller layout, false skips the controller layout but keeps the root layout, null skips all layouts, and undefined (omitted) inherits the controller layout.
Head data is not a @Render option — set defaults via defaultHead on RenderModule.forRoot(), and per-page values by returning head from the controller.
Representation results
A rendered controller can return plain props, a RenderResponse, or an explicit representation result.
import { api, page, representations } from '@nestjs-ssr/react';
representations({
html: page({ props, head, layoutProps }),
json: api(dto),
});| Factory | Returns | Notes |
|---|---|---|
page(value) | PageRepresentation<T> | HTML representation; segments are derived from it |
api(value, { mediaType? }) | ApiRepresentation<T> | JSON representation; defaults to application/json |
representations({ html, json }) | RepresentationResult | Offers both; only the negotiated one is built |
Each factory also accepts (signal: AbortSignal) => value, so the representation that is not selected is never evaluated and selected data work can stop on a deadline or disconnect. A zero-argument function remains valid.
RepresentationPolicy
Accepted by RenderModule.forRoot({ representation }) and by @Render(Component, { representation }). Route policy overrides module policy field by field; limits may only be tightened.
| Field | Type | Default | Description |
|---|---|---|---|
html | boolean | true | Offer an HTML representation |
json | boolean | false | Offer a JSON representation |
default | 'html' | 'json' | 'html' | Served when the request expresses no preference |
limits.maxBytes | number | 2097152 | Maximum size of each public payload group |
limits.maxDepth | number | 64 | Maximum payload nesting depth |
limits.mode | 'warn' | 'enforce' | 'warn' | Report or reject invalid public payloads |
deadlineMs | number | module timeout | Render deadline for this scope |
cache | CachePolicy | not sent | Cache stance — configuring it turns the stage on |
securityHeaders | SecurityHeadersPolicy | not sent | Response security headers — configuring turns on |
mandatory | PolicyField[] | [] | Module-only fields routes cannot override |
Routes may only tighten payload limits and deadlines, and cannot change module limits.mode from enforce back to warn. A field listed in module mandatory cannot be overridden at route scope.
CachePolicy: visibility ('private' \| 'public'), noStore, maxAge, sMaxAge, staleWhileRevalidate, keys (extra Vary fields).
SecurityHeadersPolicy: nosniff, referrerPolicy (string or false), contentSecurityPolicy (string with {nonce} placeholder, or false).
@Layout(component, options?)
@Layout(DashboardLayout)
@Layout(DashboardLayout, { props: { sidebar: true } })| Option | Type | Description |
|---|---|---|
props | object | Props for layout |
Hooks
Import from @nestjs-ssr/react/client (or @nestjs-ssr/react on the server).
| Hook | Returns |
|---|---|
usePageContext<T>() | Full context object |
useRequest<T>() | Alias for usePageContext() |
useParams() | Record<string, string> |
useQuery() | Record<string, string | string[]> |
useHeaders() | Headers opted in via allowedHeaders |
useHeader(name) | string | undefined |
useCookies() | Cookies opted in via allowedCookies |
useCookie(name) | string | undefined |
useNavigationState() | 'idle' or 'loading' |
useIsNavigating() | boolean |
useNavigate() | (href, options?) => Promise<void> |
useNavigation() | { state, navigate } |
Headers and cookies are opt-in only — nothing is exposed to the client until you list it in allowedHeaders / allowedCookies on RenderModule.forRoot().
createSSRHooks<T>()
Creates hooks bound to your own extended context type, so you don't pass a generic at every call site.
// src/lib/ssr-hooks.ts
import { createSSRHooks, RenderContext } from '@nestjs-ssr/react';
interface AppRenderContext extends RenderContext {
user?: { id: string; name: string };
}
export const { usePageContext, useParams, useCookie } =
createSSRHooks<AppRenderContext>();// src/views/home.tsx
import { usePageContext } from '@/lib/ssr-hooks';
export default function Home() {
const { user } = usePageContext(); // fully typed
return <h1>Welcome {user?.name}</h1>;
}Navigation
Link
import { Link } from '@nestjs-ssr/react/client';
<Link href="/products">Products</Link>
<Link href="/about" replace>About</Link>
<Link href="/settings" scroll={false}>Settings</Link>LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement>, so any anchor attribute (className, target, aria-*, …) is accepted.
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | Target URL |
replace | boolean | false | replaceState vs pushState |
scroll | boolean | true | Scroll to top after navigation |
Falls back to a normal browser navigation for external origins, modified clicks (ctrl/cmd/shift/alt), middle-clicks, and target="_blank".
navigate()
import { navigate } from '@nestjs-ssr/react/client';
await navigate('/dashboard');
await navigate('/settings', { replace: true, scroll: false });| Option | Type | Default | Description |
|---|---|---|---|
replace | boolean | false | replaceState vs pushState |
scroll | boolean | true | Scroll to top after navigation |
Types
PageProps<T>
Controller data is spread directly as component props — there is no data wrapper. head is merged in as an optional extra prop.
type PageProps<TProps = {}> = TProps & {
head?: HeadData;
};interface ProductPageProps {
product: Product;
related: Product[];
}
export default function ProductDetail({
product,
related,
head,
}: PageProps<ProductPageProps>) {
/* … */
}HeadData
Returned as head from a controller, or set globally as defaultHead. All Open Graph fields are flat (ogTitle, not og.title).
interface HeadData {
title?: string;
description?: string;
keywords?: string;
canonical?: string;
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
links?: Array<{
rel: string;
href: string;
as?: string;
type?: string;
integrity?: string;
crossorigin?: string;
referrerpolicy?: string;
}>;
meta?: Array<{
name?: string;
property?: string;
content?: string;
charset?: string;
}>;
}Values are HTML-escaped on render. Attributes are selected from element-specific allowlists; executable event attributes such as onload and onerror are discarded.
Not yet implemented
The HeadData type also declares scripts, jsonLd, htmlAttributes, and bodyAttributes. These are not rendered by any current renderer — setting them has no effect. They are tracked for a future release; don't rely on them.
RenderResponse<T>
Controllers may return a plain object (auto-wrapped as props) or the explicit shape:
interface RenderResponse<T = any> {
props: T;
head?: HeadData;
layoutProps?: Record<string, any>;
}RenderModule.forRoot(config?)
RenderConfig — every option is optional.
| Option | Type | Default | Description |
|---|---|---|---|
mode | 'string' | 'stream' | 'string' | renderToString vs renderToPipeableStream |
project | string | auto | Nest CLI project name in a monorepo workspace |
viewsDir | string | views | Override the views directory, relative to the project root or absolute |
environment | 'development' | 'production' | from NODE_ENV | Explicit override; takes precedence over NODE_ENV |
timeout | number | 10000 | Maximum SSR render duration in milliseconds |
vite | { port?, allowedHosts?, allowedOrigins? } | { port: 5173 } | Dev server and explicit remote proxy allowlists |
template | string | built-in | Custom HTML template — file path or template string |
defaultHead | HeadData | — | Default head data for all pages; per-page head overrides it |
allowedHeaders | string[] | [] | Request headers exposed to the client |
allowedCookies | string[] | [] | Cookies exposed to the client |
context | ContextFactory | — | Per-request factory merged into RenderContext |
cspNonce | CspNonceFactory | — | Per-request nonce applied to all injected script tags |
representation | RepresentationPolicy | HTML only | Representations, limits, deadline, cache, and security headers |
projectContext | ({ context, req, signal }) => RenderContext | — | Narrow the render context before it is serialized |
jsonApi | boolean | false | Deprecated — use representation.json instead |
clientNavigation | boolean | true | Serve JSON segment responses for <Link> navigation |
errorPageDevelopment | ComponentType<ErrorPageDevelopmentProps> | built-in | Custom dev error page |
errorPageProduction | ComponentType | built-in | Custom production error page |
Environment detection
Development mode is opt-in. environment wins when set; otherwise development requires NODE_ENV=development exactly. Anything else — production, test, or unset — runs the production pipeline.
This is deliberate. Development mode installs a proxy to the Vite dev server (which serves project sources, including Vite's /@fs/ file endpoint) and renders error pages containing stack traces. A deployment that forgets to set NODE_ENV gets neither.
The init CLI writes NODE_ENV=development into the generated dev:nest script. Projects that predate it need to add it:
{
"scripts": {
"dev:nest": "NODE_ENV=development nest start --watch --watchAssets --preserveWatchOutput"
}
}vite.allowedHosts and vite.allowedOrigins
The dev proxy only serves loopback clients. Requests from any other address fall through to the application, and off-machine Vite WebSocket handshakes are dropped — otherwise NestJS (which binds every interface) would republish a dev server that Vite deliberately binds to localhost only.
For a browser on another host, add its exact hostname to allowedHosts and its exact HTTP(S) origin (including port) to allowedOrigins. Both must match. Host, Origin, peer address, and forwarding headers are validated to resist DNS rebinding and accidental exposure through a local reverse proxy. Prefer an SSH tunnel where you can. These options have no effect in production.
Monorepo workspaces
SSR paths are resolved from nest-cli.json. In a single-app workspace nothing needs configuring. In a monorepo, each app resolves against its own project root, so views, the dev entries, and dist/client / dist/server are found per app rather than relative to the directory the process happens to start in.
The active project is resolved in this order:
projectpassed toRenderModule.forRoot()- the
NEST_SSR_PROJECTenvironment variable - the path of the running main file
- the default project in
nest-cli.json
// apps/web/src/app.module.ts
RenderModule.forRoot({ project: 'web' });Set viewsDir when the views live somewhere other than <sourceRoot>/views:
RenderModule.forRoot({ project: 'web', viewsDir: 'src/ui/pages' });The embedded SSR server loads the same vite.config your client build uses, so plugins, aliases, and define values apply to both halves of the render.
context factory
RenderModule.forRoot({
context: ({ req }) => ({ user: req.user }),
});
// async, with injected services
RenderModule.forRootAsync({
imports: [PermissionModule],
inject: [PermissionService],
useFactory: (permissions: PermissionService) => ({
context: async ({ req }) => ({
user: req.user,
permissions: await permissions.getForUser(req.user),
}),
}),
});cspNonce factory
RenderModule.forRoot({
cspNonce: ({ req }) => (req as any).res?.locals?.cspNonce,
});