🚀 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.
