Skip to main content
Learn how to set up and use Server-Side Rendering (SSR) in Sonamu.

SSR Overview

TanStack Router

File-based routing Type-safe navigation

Vite SSR

Fast builds HMR support

Direct API Call

No HTTP overhead Fast response

TanStack Query

Automated Hydration Caching integration

Sonamu SSR Architecture

Sonamu implements SSR using TanStack Router + Vite.

Core Components

entry-server.generated.tsx

Entry point for rendering React on the server (auto-generated):
// web/src/entry-server.generated.tsx
import { QueryClient, dehydrate } from "@tanstack/react-query";
import { createMemoryHistory, createRouter, RouterProvider } from "@tanstack/react-router";
import { Suspense } from "react";
import { renderToString } from "react-dom/server";
import { routeTree } from "./routeTree.gen";

export type PreloadedData = {
  queryKey: any[];
  data: any;
};

export async function render(url: string, preloadedData: PreloadedData[] = []) {
  // Create QueryClient
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5000,
        retry: false,
      },
    },
  });

  // Inject preloaded data directly into queryClient
  for (const { queryKey, data } of preloadedData) {
    queryClient.setQueryData(queryKey, data);
  }

  // Dehydrate
  const dehydratedState = dehydrate(queryClient);

  // Create memory history for SSR
  const memoryHistory = createMemoryHistory({
    initialEntries: [url],
  });

  // Create Router (SSR mode)
  const router = createRouter({
    routeTree,
    context: { queryClient },
    history: memoryHistory,
    defaultPreload: "intent",
  });

  // Initialize router: Must call await router.load() in SSR
  await router.load();

  // Render only RouterProvider (wrapped in Suspense - prevents hydration mismatch)
  const appHtml = renderToString(
    <Suspense fallback={null}>
      <RouterProvider router={router} />
    </Suspense>,
  );

  return {
    html: appHtml,
    dehydratedState,
  };
}

entry-client.tsx

Entry point for hydrating React on the client:
// web/src/entry-client.tsx
import { hydrate, QueryClient } from "@tanstack/react-query";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import ReactDOM from "react-dom/client";
import { routeTree } from "./routeTree.gen";
import { dateReviver } from "./services/sonamu.shared";

// SSR data type
declare global {
  interface Window {
    __SONAMU_SSR__?: any;
    __SONAMU_SSR_CONFIG__?: {
      disableHydrate?: boolean;
    };
  }
}

// Create QueryClient
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5000,
      retry: false,
      refetchOnMount: true,
    },
  },
});

// Restore SSR data
const dehydratedState = window.__SONAMU_SSR__
  ? JSON.parse(JSON.stringify(window.__SONAMU_SSR__), dateReviver)
  : undefined;

if (dehydratedState) {
  hydrate(queryClient, dehydratedState);
}

// Create Router
const router = createRouter({
  routeTree,
  context: { queryClient },
  defaultPreload: "intent",
});

await router.load();

// Hydration
if (document.documentElement.innerHTML && dehydratedState) {
  // SSR page - Hydration
  ReactDOM.hydrateRoot(document, <RouterProvider router={router} />);
} else {
  // Pure CSR page - Render new
  ReactDOM.createRoot(document).render(<RouterProvider router={router} />);
}

TanStack Router Route Structure

Root Route

// web/src/routes/__root.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRootRouteWithContext, HeadContent, Outlet, Scripts } from "@tanstack/react-router";

export interface RouterContext {
  queryClient: QueryClient;
}

export const Route = createRootRouteWithContext<RouterContext>()({
  head: () => ({
    meta: [
      { charSet: "utf-8" },
      { name: "viewport", content: "width=device-width, initial-scale=1.0" },
      { title: "My App" },
    ],
  }),
  component: RootComponent,
});

function RootComponent() {
  const { queryClient } = Route.useRouteContext();

  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <div id="root">
          <QueryClientProvider client={queryClient}>
            <Outlet />
          </QueryClientProvider>
        </div>
        <Scripts />
      </body>
    </html>
  );
}

File Route

// web/src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  component: HomePage,
});

function HomePage() {
  return (
    <div>
      <h1>Home Page</h1>
    </div>
  );
}

Dynamic Route

// web/src/routes/users/$id.tsx
import { createFileRoute } from "@tanstack/react-router";
import { UserService } from "@/services/services.generated";

export const Route = createFileRoute("/users/$id")({
  component: UserPage,
});

function UserPage() {
  const { id } = Route.useParams();
  const { data } = UserService.useUser("C", parseInt(id));

  return (
    <div>
      <h1>{data?.user.username}</h1>
      <p>{data?.user.email}</p>
    </div>
  );
}

Vite Configuration

// web/vite.config.ts
import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig(({ isSsrBuild }) => ({
  plugins: [
    react(),
    tanstackRouter({
      autoCodeSplitting: true,
      generatedRouteTree: "./src/routeTree.gen.ts",
    }),
  ],
  build: {
    outDir: "dist/client",
    rollupOptions: {
      output: isSsrBuild
        ? {}
        : {
            manualChunks: {
              "vendor-react": ["react", "react-dom"],
              "vendor-tanstack": ["@tanstack/react-query", "@tanstack/react-router"],
            },
          },
    },
  },
  ssr: {
    noExternal: true, // Include all dependencies in bundle for production build
  },
}));

SSR vs CSR

Sonamu uses a hybrid approach:

SSR Rendering (when data preloading)

// Routes registered with registerSSR
// β†’ Generate HTML on server + include data
// β†’ Hydration on client
Benefits:
  • Fast First Contentful Paint
  • SEO optimization
  • Immediate display of initial data

CSR Rendering (default)

// Routes without registerSSR
// β†’ Render directly on client
// β†’ Load data via API call
Benefits:
  • Reduced server load
  • Simple implementation
  • Fast development

Development vs Production

Development Mode

sonamu dev            # Integrated mode (= sonamu dev all)
sonamu dev all        # Integrated mode (one-port: API + Web)
sonamu dev api        # API-only mode (Vite integration disabled)
sonamu dev web        # Vite standalone
sonamu dev web -- --port 5173 --host 0.0.0.0  # Pass Vite options
  • dev all: API server integrates Vite to serve API + Web on a single port (HMR supported)
  • dev api: Runs API only. Use when Web is not needed or when running Vite separately
  • dev web: Runs Vite dev server standalone. Pass Vite options after --

Production Build

sonamu build          # Full build (= sonamu build all)
sonamu build api      # API only
sonamu build web      # Web only
Build output:
web/dist/                               # Web build source
β”œβ”€β”€ client/                             # Client bundle
└── server/                             # SSR bundle

api/web-dist/                           # Copied from web/dist (for deployment)
β”œβ”€β”€ client/                             # = web/dist/client copy
β”‚   β”œβ”€β”€ index.html
β”‚   └── assets/
└── server/                             # = web/dist/server copy
    └── entry-server.generated.js

api/dist/                               # API build output
└── ssr/
    └── routes.js                       # SSR routes (API owned)

Cautions

Cautions when using Sonamu SSR: 1. TanStack Router based: Not Next.js App Router 2. entry-server.generated.tsx is auto-generated: Don’t modify directly 3. Be careful with window object: window doesn’t exist on server 4. dateReviver required: Handles Date object serialization/deserialization 5. Use registerSSR: See next document for data preloading

Next Steps

Data Preloading

How to use registerSSR

Hydration Strategies

Hydration optimization

Cache Control

TanStack Query caching

TanStack Router Docs

Router detailed guide