Skip to content
elephantoo

Deploying React apps

Lesson 29 of 30 14 min read

Production builds, environment variables, SPA routing, and deploying to Vercel, Netlify and Firebase Hosting.


You've built something great — now let's put it on the internet. A Vite + React app compiles to static files (HTML, CSS, JavaScript and assets), so it can be hosted on any static host, often free, with HTTPS and a global CDN. This lesson covers building for production, environment variables, and deploying to Vercel, Netlify and Firebase Hosting.

Building for production#

Terminal
npm run build
Output
vite v8.3.1 building client environment for production...
transforming...
✓ 42 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html                   0.46 kB │ gzip:  0.30 kB
dist/assets/index-DiwrgTda.css    1.39 kB │ gzip:  0.71 kB
dist/assets/index-BqKa7F2x.js   196.41 kB │ gzip: 61.85 kB
✓ built in 412ms

Vite minifies your code, removes development-only checks, splits chunks and adds content hashes to file names (index-BqKa7F2x.js). The hash changes only when the file changes, so browsers can cache assets forever.

Always test the build locally before deploying:

Terminal
npm run preview
Output
  ➜  Local:   http://localhost:4173/

If something works in npm run dev but breaks in preview, you've caught it before your users did.

Environment variables#

Different environments need different settings — an API URL for development, another for production. Vite loads them from .env files:

.env
VITE_API_URL=http://localhost:3000
VITE_APP_NAME=Elephantoo
.env.production
VITE_API_URL=https://api.elephantoo.com

Read them with import.meta.env:

JSX
const API_URL = import.meta.env.VITE_API_URL;

export async function getCourses() {
  const res = await fetch(`${API_URL}/courses`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

export function Footer() {
  return (
    <footer>
      {import.meta.env.VITE_APP_NAME} · {import.meta.env.MODE}
      {import.meta.env.DEV && " (development build)"}
    </footer>
  );
}

Key rules:

  • Only variables prefixed with VITE_ are exposed to your code. Others stay private to the build.
  • Values are replaced at build time and end up as plain text in your JavaScript. Never put secrets (API keys with write access, database passwords) in VITE_ variables — anyone can read them in the browser. Keep secrets on a server.
  • .env.production is used by vite build; .env.development by vite dev. Add .env.local (and .env.*.local) to .gitignore for personal overrides.
  • Built-ins: import.meta.env.MODE, DEV, PROD and BASE_URL.
  • Changing a variable requires a rebuild (and a dev-server restart).

On hosting platforms, set the same variables in the project's settings dashboard so they're available during the build.

The SPA fallback (don't skip this!)#

With React Router, /courses/react is handled by JavaScript in index.html. There's no courses/react file on the server, so refreshing that URL or opening a shared link gives a 404 — unless you tell the host to serve index.html for unknown paths. Each host has its own way, shown below.

Deploying to Vercel#

Option 1 — connect Git (recommended):

  1. Push your project to GitHub, GitLab or Bitbucket.
  2. Go to vercel.com → Add New… → Project and import the repository.
  3. Vercel detects Vite and fills in Build command npm run build and Output directory dist.
  4. Add environment variables, then click Deploy.

Every push to main deploys to production, and every pull request gets its own preview URL — great for reviewing changes.

Option 2 — the CLI:

Terminal
npm install -g vercel
vercel          # deploy a preview
vercel --prod   # deploy to production

Add the SPA fallback with a vercel.json in the project root:

vercel.json
{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}

Real files (like /assets/index-BqKa7F2x.js) are still served first; only unmatched paths fall back to index.html.

Deploying to Netlify#

Connect your repository at app.netlify.com (Add new site → Import an existing project), or describe the build in a netlify.toml:

netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

The status = 200 makes it a rewrite (the URL stays the same) rather than a redirect. Alternatively, put a _redirects file in public/ containing /* /index.html 200. Netlify also offers deploy previews and a CLI:

Terminal
npm install -g netlify-cli
netlify deploy          # draft URL
netlify deploy --prod   # production

Deploying to Firebase Hosting#

Firebase Hosting is a fast, free-tier-friendly option, especially if you use other Firebase services (Auth, Firestore).

Terminal
npm install -g firebase-tools
firebase login
firebase init hosting

Answer the prompts:

Output
? What do you want to use as your public directory? dist
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
? Set up automatic builds and deploys with GitHub? No

This creates firebase.json:

firebase.json
{
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [{ "source": "**", "destination": "/index.html" }],
    "headers": [
      {
        "source": "/assets/**",
        "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
      }
    ]
  }
}

Then build and deploy:

Terminal
npm run build
firebase deploy --only hosting
Output
✔  Deploy complete!

Hosting URL: https://your-project.web.app

The headers block tells browsers to cache hashed assets for a year — safe, because a new build produces new file names. Use firebase hosting:channel:deploy preview for temporary preview URLs.

Deploying to a sub-path (e.g. GitHub Pages)#

If your app lives at https://user.github.io/my-app/, set Vite's base so asset URLs include the sub-path:

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

export default defineConfig({
  plugins: [react()],
  base: "/my-app/",
});

Pass the same value to your router: createBrowserRouter(routes, { basename: "/my-app" }). GitHub Pages has no rewrite support, so SPAs there usually need a 404.html copy of index.html or hash-based routing.

Automating with CI#

Run checks before every deploy so broken code never ships. A minimal GitHub Actions workflow:

.github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npx vitest run
      - run: npm run build

Combine this with Vercel/Netlify Git integration: CI checks every pull request, and the host deploys when you merge.

Before you launch: a checklist#

  • npm run build && npm run preview works, with no console errors
  • Refreshing on a deep link (e.g. /courses/react) works — SPA fallback configured
  • Environment variables are set on the host; no secrets in VITE_ variables
  • A 404 page and a top-level error boundary exist
  • Page <title> and meta description are set
  • Custom domain connected, with HTTPS (all three hosts provide free certificates)
  • Lighthouse scores checked for performance and accessibility
  • Error monitoring (e.g. Sentry) and analytics are set up if you need them

Common mistakes#

  • Forgetting the SPA rewrite, so deep links 404.
  • Wrong output directory (build instead of Vite's dist).
  • Putting secrets in VITE_ variables, or expecting process.env to work in browser code.
  • Changing environment variables on the host but not redeploying.
  • Committing .env.local files with private values.
  • Testing only in dev mode and discovering production-only issues after launch.

What's next#

Static hosting is perfect for many apps. For SEO-critical pages, server rendering and full-stack features, the next step is a framework: Next.js and React Server Components.

Check your understanding

Quick quiz

0/3 answered
  1. 1.In a Vite app, which environment variable can your React code read?

  2. 2.After deploying, /courses/react works when you click a link, but refreshing gives a 404. Why?

  3. 3.What does npm run build produce in a Vite project?

Finished reading?

Mark this lesson complete to track your progress.