🚀 Why I Did This
Next.js is great, but I wanted to see how much I could do without it. Like… what’s really needed to get SSR working? How far can I go using tools I understand and can control?
This setup gave me that. 😄
🛠 What I Used
- React 19
- Vite
- Vike
- Express
- TypeScript
🔧 Step-by-Step Setup
1. Create a React + Vite Project
npm create vite@latest
Then I just followed the prompts:
- Named the project: sample-project
- Select a framework: React
- Select a variant: TypeScript
cd sample-project npm install
2. Install Dependencies and devDependencies
npm install vike express npm install --save-dev @types/node @types/express
3. Update vite.config.ts
I kept it simple:
import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], });
Vike doesn’t need a plugin here — it figures things out.
4. Create an Express Server
I made a server/index.ts
that looks like this:
import express from "express"; import { renderPage } from "vike/server"; import { fileURLToPath } from "url"; import { dirname, resolve } from "path"; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, ".."); async function startServer() { const app = express(); app.use(express.static(`${root}/dist/client`)); app.get("*", async (req, res, next) => { const pageContext = await renderPage({ urlOriginal: req.originalUrl }); if (!pageContext.httpResponse) return next(); const { body, statusCode, contentType } = pageContext.httpResponse; res.status(statusCode).type(contentType).send(body); }); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); }); } startServer();
5. Modify tsconfig files
Create a `tsconfig.server.json`
{ "compilerOptions": { "target": "ES2022", "lib": ["ES2022"], "module": "ESNext", "moduleResolution": "node", "jsx": "react-jsx", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "skipLibCheck": false, "isolatedModules": true, "noEmit": true, "strict": true, "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["server/**/*", "pages/**/*"] // <-- to include newly created folders }
then modify `tsconfig.json` to add `tsconfig.server.json` as path reference:
{ "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }, { "path": "./tsconfig.server.json" } // <-- add this ] }
6. Set Up My Page
Inside `pages/` I created a `+Page.tsx`:
function Page() { return ( <div> <h1>Welcome to my React 19 SSR App!</h1> </div> ); } export default Page;
No route config needed, since this is the default route.
6. Render HTML Server-Side
`/pages/+onRenderHtml.tsx`
import React from "react"; import { renderToString } from "react-dom/server"; export { onRenderHtml }; import { escapeInject, dangerouslySkipEscape } from "vike/server"; // eslint-disable-next-line @typescript-eslint/no-explicit-any async function onRenderHtml(pageContext: any) { const { Page, pageProps } = pageContext; const pageHtml = Page ? renderToString(<Page {...pageProps} />) : ""; return escapeInject`<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Vike + React 19 SSR</title> </head> <body> <div id="page-view">${dangerouslySkipEscape(pageHtml)}</div> </body> </html>`; }
This is the file Vike looks for when it asks, "how should I turn this React component into HTML?"
7. Create a `+config.ts`
export default { route: "/", };
8. Scripts in My package.json
"scripts": { "dev": "vike dev", "build": "vike build", "preview": "vike build && vike preview" },
I don’t use nodemon anymore — just vike dev
for dev and it works great. 🔥
🎉 And That’s It!
I ran:
npm run dev
Then opened http://localhost:3000
And boom 💥 — my page rendered, server-side, from React 19!
💡 What I Learned
- Vike is very picky about file names — but for good reason.
- You can get SSR working with just a few files.
- You don’t always need Next.js if you’re willing to get your hands dirty.
- This setup gave me more appreciation for how things actually work behind the scenes.
🔜 Next Goals
- Add hydration so it’s interactive
- Try a dynamic route like
/blog/:slug
- Add an error page (because mine was just crashing during the initial setup 😅)
- Deploy this somewhere fun (maybe render.com or Netlify Functions?)
💬 Final Thoughts
This was super fun to set up. If you like learning and messing with code until it makes sense, you’ll enjoy this too. It’s not plug-and-play — but it’s yours.
Let me know if you’re trying this too! 💌
Or if you ran into a weird error and need a buddy to debug with just let me know.