Skip to content

Build a Full-Stack React App with Vite SSR in 5 Minutes

By childrentime · April 3, 2026

Vite is the fastest dev server in the JavaScript ecosystem. But using it for SSR has always meant wiring up renderToPipeableStream, configuring client/server builds, and handling hydration yourself.

Pareto is a React SSR framework built on Vite 7 that handles all of that. You get file-based routing, streaming SSR, loaders, state management, and a 62 KB client bundle — with zero config.

Let’s build a full-stack React app in 5 minutes.

Terminal window
npx create-pareto@latest my-app
cd my-app
npm install
npm run dev

Open http://localhost:3000. You should see the default page. Edit app/page.tsx and watch it hot-reload instantly via Vite’s HMR.

2. Understand the project structure (30 seconds)

Section titled “2. Understand the project structure (30 seconds)”
my-app/
app/
layout.tsx # Root layout (header, nav, footer)
page.tsx # Homepage (/)
head.tsx # Root <title> and meta tags
not-found.tsx # 404 page
globals.css # Global styles
pareto.config.ts # Framework config (optional)
package.json
tsconfig.json

Every directory inside app/ with a page.tsx becomes a route. Nested directories create nested routes. That’s it.

3. Build a page with server data (1 minute)

Section titled “3. Build a page with server data (1 minute)”

Create a new route at /posts:

app/posts/loader.ts
import type { LoaderContext } from '@paretojs/core'
export function loader(ctx: LoaderContext) {
// This runs on the server only
return {
posts: [
{ id: 1, title: 'Hello World', body: 'First post' },
{ id: 2, title: 'Vite SSR', body: 'It is fast' },
],
}
}
app/posts/page.tsx
import { useLoaderData } from '@paretojs/core'
interface Post {
id: number
title: string
body: string
}
export default function PostsPage() {
const { posts } = useLoaderData<{ posts: Post[] }>()
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
)
}
app/posts/head.tsx
export default function Head() {
return (
<>
<title>Posts — My App</title>
<meta name="description" content="All blog posts" />
</>
)
}

Navigate to http://localhost:3000/posts. The loader runs on the server, the HTML is server-rendered, and the page hydrates on the client. View source — the posts are in the HTML.

Real apps fetch from databases and APIs. Some are fast, some are slow. Use defer() to stream slow data without blocking the page:

app/dashboard/loader.ts
import { defer } from '@paretojs/core'
async function getQuickStats() {
return { users: 1_234, pageViews: 56_789 }
}
async function getSlowAnalytics() {
// Simulate a slow API call
await new Promise((r) => setTimeout(r, 2000))
return { topPage: '/posts', bounceRate: 0.42 }
}
export async function loader() {
const stats = await getQuickStats() // resolve fast data first
return defer({
stats, // resolved — in initial HTML
analytics: getSlowAnalytics(), // Promise — streamed later
})
}
app/dashboard/page.tsx
import { useLoaderData, Await } from '@paretojs/core'
export default function DashboardPage() {
const { stats, analytics } = useLoaderData()
return (
<div>
<h1>Dashboard</h1>
<p>{stats.users} users · {stats.pageViews} page views</p>
<Await resolve={analytics} fallback={<p>Loading analytics...</p>}>
{(data) => (
<div>
<p>Top page: {data.topPage}</p>
<p>Bounce rate: {(data.bounceRate * 100).toFixed(0)}%</p>
</div>
)}
</Await>
</div>
)
}

Visit http://localhost:3000/dashboard. The stats appear immediately. The analytics section shows “Loading analytics…” then streams in after 2 seconds. The page never blocks.

5. Add client-side navigation (30 seconds)

Section titled “5. Add client-side navigation (30 seconds)”

Use <Link> for SPA-style navigation without full page reloads:

app/layout.tsx
import type { PropsWithChildren } from 'react'
import { Link } from '@paretojs/core'
export default function RootLayout({ children }: PropsWithChildren) {
return (
<>
<nav>
<Link to="/">Home</Link>
<Link to="/posts">Posts</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
<main>{children}</main>
</>
)
}

Clicks navigate instantly. Loader data is fetched via NDJSON streaming — deferred data streams in progressively, just like the initial SSR render.

Pareto ships defineStore() with Immer — no extra dependencies:

app/stores/theme.ts
import { defineStore } from '@paretojs/core/store'
export const themeStore = defineStore((set) => ({
mode: 'light' as 'light' | 'dark',
toggle: () => set((d) => {
d.mode = d.mode === 'light' ? 'dark' : 'light'
}),
}))
// Use in any component
import { themeStore } from '../stores/theme'
function ThemeToggle() {
const { mode, toggle } = themeStore.useStore()
return <button onClick={toggle}>Theme: {mode}</button>
}

State is automatically serialized during SSR and hydrated on the client. No boilerplate.

Create a route.ts file for JSON API endpoints:

app/api/time/route.ts
import type { LoaderContext } from '@paretojs/core'
export function loader(ctx: LoaderContext) {
return { time: new Date().toISOString() }
}

GET http://localhost:3000/api/time returns {"time":"2026-04-03T..."}. Standard REST endpoints, no extra setup.

Terminal window
npm run build
npm run start

That’s it. Your production server is a standard Node.js process running Express + Vite’s optimized build. Deploy it anywhere you run Node: Docker, Fly.io, Railway, a VPS, Kubernetes.

No special hosting requirements. No serverless runtime compatibility issues. No vendor lock-in.

In 5 minutes, you have:

  • File-based routing — directories map to routes
  • Server-side rendering — full HTML on first load, great for SEO
  • Streaming SSR — slow data doesn’t block the page
  • Client navigation — SPA-feel with NDJSON streaming
  • Head management — per-route <title> and meta tags via React components
  • State management — Immer-powered stores with automatic SSR hydration
  • API endpoints — JSON routes alongside your pages
  • TypeScript — full type safety across loaders and components
  • 62 KB client bundle — 73% smaller than Next.js

All powered by Vite 7 — instant dev server startup, React Fast Refresh, and native ESM in development.

Vite (Pareto)Webpack (Next.js)Turbopack (Next.js)
Dev server startInstant (native ESM)Seconds (bundling)Fast (incremental)
HMRReact Fast RefreshReact Fast RefreshReact Fast Refresh
Plugin ecosystemVite/Rollup pluginsWebpack loadersLimited
Config complexityOne pareto.config.tsnext.config.js + morenext.config.js + more
Build outputOptimized Rollup bundleWebpack bundleWebpack bundle

Vite’s native ESM dev server means zero bundling during development. Your 100-route app starts as fast as your 1-route app.

Terminal window
npx create-pareto@latest my-app
cd my-app && npm install && npm run dev