Image from undraw.coImage from undraw.co

Building React 19 + Vite + SSR Without Next.js (Using Vike)

April 20, 2025

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