Vite + React SPA on Cloudflare Workers

The simplest deployment: a client-side React app built with Vite, deployed as static assets on Workers. No SSR, no server code, zero cold starts. Uses the CF Vite Plugin for static asset serving configuration.

Create a New Project

npm create cloudflare@latest my-spa -- --framework=react

Or add Cloudflare to an existing Vite + React project:

npm install -D @cloudflare/vite-plugin

Project Structure

my-spa/
├── src/
│   ├── App.tsx          # Root component
│   ├── main.tsx         # Entry point
│   └── index.css
├── public/              # Static files
├── index.html           # HTML template
├── vite.config.ts       # Vite + CF plugin
└── wrangler.jsonc       # Cloudflare config

Vite Config

// vite.config.ts
import { cloudflare } from "@cloudflare/vite-plugin";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    cloudflare(),
    react(),
  ],
});

App Component

// src/App.tsx
import { useState, useEffect } from "react";

function App() {
  const [data, setData] = useState<string | null>(null);

  useEffect(() => {
    // Fetch from your API (could be a separate Hono Worker)
    fetch("/api/hello")
      .then((r) => r.json())
      .then((d) => setData(d.message));
  }, []);

  return (
    <div>
      <h1>React SPA on Cloudflare</h1>
      <p>{data || "Loading..."}</p>
    </div>
  );
}

export default App;

Wrangler Config

// wrangler.jsonc
{
  "name": "my-spa",
  "assets": {
    "directory": "./dist"
  }
}

That’s the entire config. No main field needed since there’s no server code.

Adding an API Alongside the SPA

If you need a backend, you can add a Worker entry point:

// worker.ts
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname.startsWith("/api/")) {
      // Handle API routes
      if (url.pathname === "/api/hello") {
        return Response.json({ message: "Hello from Worker" });
      }
      return new Response("Not found", { status: 404 });
    }

    // Static assets are served automatically by Workers Static Assets
    return new Response("Not found", { status: 404 });
  },
};
// wrangler.jsonc (with API)
{
  "name": "my-spa",
  "main": "./worker.ts",
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS"
  }
}

Dev and Deploy

# Development
npm run dev

# Build
npm run build

# Deploy
npx wrangler deploy

Key Points

  • Zero server code by default, all static assets
  • No cold starts, globally cached via Cloudflare CDN
  • CF Vite Plugin handles asset configuration
  • Can add a Worker entry point for API routes if needed
  • Best for: dashboards, admin panels, tools, any app where SEO doesn’t matter
  • Not for: content sites needing SEO (use Astro or React Router with SSR)

Tip: For SPAs that need an API, consider deploying a separate Hono Worker for the API and using this SPA for the frontend. Service bindings let them communicate efficiently.

Gotcha: SPA routing (React Router, etc.) requires a catch-all that serves index.html for all paths. Workers Static Assets handles this automatically when there’s no Worker entry point. If you add a Worker, make sure unmatched routes fall through to static assets.