Skip to content
@nestjs-ssr/react

React SSR for NestJS

One app. One deploy. Full type safety from database to DOM.

0.4: NestJS 11 and 12, per-page code loading, view edits without restarts.

🔒

One Type, Both Sides

Controller return type is the component's props. Change one, the other breaks at build time.

🏗️

Layouts That Stay Put

Nested layouts persist across navigations. Header, sidebar, shell — rendered once, never re-mounted.

⚡

Stream or String

renderToString for simplicity, renderToPipeableStream for performance. Suspense boundaries stream as they resolve.

🧭

No Full Reloads

Link clicks fetch only the changed segment. The rest of the page stays alive — state, scroll, animations.

You don't need a second app ​

The standard approach to SSR with a backend framework: build the API, then build a separate frontend app that calls it. Whether it's Next.js, TanStack Start, Remix, or a plain Vite SPA — you end up with two routers, two deploys, and a type boundary you maintain by hand.

This library takes a different approach. React lives inside your NestJS app as a view layer. Controllers return data, components render it, TypeScript enforces the contract. One process, one build, one deploy.

Nothing breaks ​

  • NestJS routing, guards, pipes, interceptors
  • Your existing services and modules
  • Server-side testing without touching React
  • Full control over the request lifecycle

Everything improves ​

  • React components as views, not a separate app
  • End-to-end type safety from DB to DOM
  • Nested layouts with persistent shared chrome
  • Vite HMR, and view edits that never restart Nest
  • Each page downloads only its own code
  • SEO out of the box: title, meta, Open Graph

Two files. That's the whole contract.

The controller declares what data to return and which component renders it. The component receives typed props. TypeScript connects them.

typescript
import { Controller, Get, Param } from '@nestjs/common';
import { Render, Layout } from '@nestjs-ssr/react';
import { RecipesLayout } from './views/recipes-layout';
import { RecipeDetail } from './views/recipe-detail';

@Controller('recipes')
@Layout(RecipesLayout)
export class RecipesController {
  @Get(':slug')
  @Render(RecipeDetail)
  getRecipe(@Param('slug') slug: string) {
    const recipe = this.recipes.findBySlug(slug);
    return {
      recipe,
      chef: this.chefs.findById(recipe.chefId),
      head: { title: recipe.name },
    };
  }
}
tsx
import { PageProps } from '@nestjs-ssr/react';

export default function RecipeDetail({
  recipe,
  chef,
}: PageProps<RecipeDetailProps>) {
  return (
    <article>
      <h1>{recipe.name}</h1>
      <p>{recipe.description}</p>
      <IngredientList items={recipe.ingredients} />
      <ChefCard chef={chef} />
    </article>
  );
}

Return the wrong shape and TypeScript catches it before the code runs.

Fast where you feel it ​

  • Pages load only their own code. Views are split per route and the server preloads the current page's chunk, so adding pages never slows the first one down.
  • Edit a view, see it in ~0.2 s. nestjs-ssr dev restarts Nest only when a controller or service changes; view edits go through Vite, and the next server render already has them.
  • Measured, and gated in CI. Server cost per request, bundle budgets, hydration timing and the dev loop all have recorded baselines. See Performance.

Released under the MIT License.