Logo Manvith Reddy Dayam
How to make modern React work without Nextjs?

How to make modern React work without Nextjs?

March 2, 2025
Table of Contents

Modern React gives you two big server-side options: SSR (server‑side rendering) and RSC (React Server Components). They can deliver great UX, but they also add complexity, lock you to specific runtimes, and complicate deployments. For many apps, a simpler architecture wins: ship a fast SPA built with Vite, proxy API calls at the edge, and host it wherever you like—Nginx on Google Cloud or Vercel.

SSR and Server Components

SSR and Server Components: what problem they solve, and what they cost

  • What they solve

    • Faster first render for content-heavy pages (SSR streams HTML).

    • Better SEO for content that must appear in HTML immediately.

    • You can keep rendering code on the server and reduce client JS.

  • The trade-offs

    • Operational complexity: SSR/RSC require a server or “edge” runtime that runs your React tree per request. That’s more moving parts than serving static files.

    • Cost and scaling: You pay for CPU time on every request; you have to worry about cold starts, tuning concurrency, and cache strategies.

    • Framework coupling: Real-world SSR/RSC is framework-specific (e.g., Next.js). Your deployment model, bundling, routing, and data fetching become coupled to that framework and runtime assumptions.

    • Debugging and boundaries: Mixing server and client trees (RSC) introduces new failure modes, waterfalls if boundaries are off, and strict rules about what can run where.

    • Caching is harder: HTML is now dynamic, so you must design caching at multiple layers (per-route, per-segment, per-user) without serving the wrong thing.

For many dashboards, internal tools, and apps with authenticated traffic where SEO isn’t the driver, SSR often add more complexity than value.

How Vite serves React

Vite is a dev-and-build tool that makes React development instant and production builds tiny.

  • Dev server: On the fly ESM and lightning-fast HMR for React. You can proxy /api to any backend so you never fight CORS locally.

  • Production build: Uses Rollup to produce hashed, cacheable assets (JS/CSS) and an index.html shell. This is perfect for static hosting behind any CDN or proxy.

  • Optional SSR: Vite can power SSR if you really need it, but you don’t have to start there.

Simple Architecture

  • In the browser, your React app always calls /api/… .

  • In development, Vite proxies /api to your real backend. No CORS, no browser config.

  • In production, Vite hosted on Nginx (or Vercel rewrites) forwards /api to your backend. You keep a single origin, avoid CORS, and keep React completely backend-agnostic.

  • You get the easy scaling of static files plus the flexibility to point /api anywhere.

Hosting Options

  • Nginx that scales like a “function” on Google Cloud: Put Nginx in a small container, serve the Vite build as static files, and reverse-proxy /api to your backend. Deploy it on Cloud Run for autoscaling, scale-to-zero, and HTTPS without managing VMs.

  • Vercel static hosting: Let Vercel build or serve your Vite dist/ and add one rewrite rule that forwards /api to your backend. You get a global CDN and zero ops.

Minimal Code Example

React + Vite (client always calls /api)

src/main.tsx
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom/client";
 
function App() {
  const [msg, setMsg] = useState("…");
 
  useEffect(() => {
    fetch("/api/health")
      .then((r) => r.json())
      .then((d) => setMsg(d.status || JSON.stringify(d)))
      .catch((e) => setMsg(`Error: ${e.message}`));
  }, []);
 
  return (
    <main style={{ fontFamily: "system-ui", padding: 24 }}>
      <h1>Vite + React</h1>
      <p>API health: {msg}</p>
    </main>
  );
}
 
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

Vite.config.ts (dev proxy for /api)

vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
 
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:8080",
        changeOrigin: true
        // rewrite: (p) => p.replace(/^\/api/, "")
      }
    }
  },
  build: { outDir: "dist" }
});
 

Nginx config (Single Page Application + /api proxy)

Use react-router, tanstack-router etc. for client side routing…

nginx.conf
server {
  listen 8080;
  server_name _;
 
  root /usr/share/nginx/html;
  index index.html;
 
  # Serve static assets fast
  location ~* \.(?:js|css|woff2?|ttf|svg)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files $uri =404;
  }
 
  # SPA fallback
  location / {
    try_files $uri $uri/ /index.html;
  }
 
  # Reverse proxy for your API
  location /api/ {
    proxy_pass http://backend.internal:8080/;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
 
    # If your backend expects no /api prefix, add:
    # rewrite ^/api/(.*)$ /$1 break;
  }
}
 

Dockerfile for Cloud Run

Dockerfile
# Build stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# Runtime: Nginx serves the built assets
FROM nginx:1.27-alpine
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html

Minimal vercel.json example if you want to deploy on vercel

vercel.json
{
  "version": 2,
  "rewrites": [{ "source": "/api/(.*)", "destination": "https://api.example.com/$1" }]
}