Himanshu SharmaSoftware engineer
Let’s talk

React · Lesson 37 of 38

Building and deploying

Build a React app with Vite, read the dist folder, use environment variables safely, set a base path, configure SPA routing and deploy to static hosting.

20 minQuality and shippingUpdated 24 October 2026

By the end of this lessonProduce a production build, configure environment variables and the base path, and put the dist folder on a web host so that every route works.

Everything so far ran on your own computer with the development server. To let other people use your app, you build it and put the result on a web server. Because a Vite React app is a single-page application, the result is just static files, so hosting is cheap and simple. This lesson walks through the build, the settings that matter, and the common places to publish.

Build for production

bash
npm run build

Vite compiles JSX, bundles all your modules and the libraries you use into a few files, minifies them (removes spaces, shortens names), splits out lazily loaded parts, and puts the output in the dist folder. For a small app the output looks like this:

text
dist/index.html                   0.45 kB │ gzip:  0.29 kB
dist/assets/index-nqMpL4T3.css    1.78 kB │ gzip:  0.81 kB
dist/assets/index-BKKMhUUY.js   219.65 kB │ gzip: 68.64 kB
✓ built in 73ms

The file names contain a hash of their content. If the content changes, the name changes, so browsers can cache files "forever" and still fetch new versions after a deploy. gzip shows the size sent over the network when the server compresses responses. The folder contains:

text
dist/
├── index.html
├── favicon.svg
├── icons.svg
└── assets/
    ├── index-BKKMhUUY.js
    └── index-nqMpL4T3.css

Only the contents of dist go on the server. The src folder, node_modules and package.json are for building, not for serving. The production build has no development overlay, no hot reloading, and no StrictMode double-rendering, and React runs in its faster production mode.

Preview it before you deploy

bash
npm run preview

This serves dist locally (the address is printed, normally http://localhost:4173/). Always look at the production build once: a few bugs appear only there (a missing environment variable, a wrong path, an import that works in development but not after bundling). When I previewed a build with a client-side router, /, /courses and a deep URL all answered with the app, as vite preview already rewrites unknown paths to index.html; your real server must do the same, as described below.

Check the size

A fast site is a small one. Look at the numbers in the build output: JavaScript above a few hundred kilobytes (gzipped) deserves attention. Ways to reduce it: split routes with React.lazy (see the Suspense lesson), avoid importing whole libraries for one function (import debounce from 'lodash/debounce', not all of lodash), compress images, and analyse the bundle with a visualiser plugin such as rollup-plugin-visualizer. Vite warns about chunks larger than 500 kB.

Environment variables

Different environments need different settings: the address of the API in development and in production, a feature flag, an analytics id. Vite reads .env files in the project root:

text
VITE_API_URL=https://api.example.com

and exposes variables whose names start with VITE_ on import.meta.env:

jsx
export default function Env() {
  return <p>API: {import.meta.env.VITE_API_URL}, mode: {import.meta.env.MODE}, prod: {String(import.meta.env.PROD)}</p>
}

Some built-in values are always there: import.meta.env.MODE ("development" or "production"), DEV and PROD (booleans), and BASE_URL. You can have several files: .env.development, .env.production and .env.local (not committed to Git) override one another by mode.

Environment variables are public

This is the point that surprises people. The values are copied into the JavaScript at build time. I built the project above with two variables in .env:

text
VITE_API_URL=https://api.example.com
SECRET_KEY=do-not-leak

and searched the output. The built index-*.js file contained https://api.example.com, exactly once, and the string do-not-leak did not appear (0 matches), because Vite ignores variables without the prefix. But anything with the VITE_ prefix is readable by anyone who opens the browser's developer tools. So:

  • Use VITE_ variables only for non-secret configuration (public URLs, public keys designed for browsers, flags).
  • Never put a database password, a payment secret key or a private API token in React code, whatever the name. If your app needs one, make a small server (or serverless function) that holds the secret and exposes only the operation you need; React calls your server.
  • Do not commit .env.local or files with real values; commit an .env.example with fake placeholders.
  • Environment values are fixed at build time. To change one you rebuild.

The base path

By default the built index.html refers to its files from the root: /assets/index-....js. If the app lives in a sub-folder such as https://example.com/shop/, those paths are wrong. Set base in vite.config.js:

javascript
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
  base: '/shop/',
})

After rebuilding, the generated page uses the prefix:

text
<script type="module" crossorigin src="/shop/assets/index-BKKMhUUY.js"></script>
<link rel="stylesheet" crossorigin href="/shop/assets/index-nqMpL4T3.css">

For a client-side router, also tell it the same base (<BrowserRouter basename="/shop">; in the Vite code, use import.meta.env.BASE_URL). GitHub Pages project sites (https://user.github.io/repo/) need exactly this.

Making client-side routes work on the server

As the routing lesson explained, with BrowserRouter the server only has index.html, so a direct visit or a refresh on /courses/react would return 404. The server must answer every URL that is not an existing file with index.html. How:

Apache (shared hosting, cPanel): put a .htaccess file next to index.html (add it to the public folder of your project so that the build copies it into dist):

text
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

Nginx:

text
location / {
  try_files $uri /index.html;
}

Netlify: a file public/_redirects containing /* /index.html 200. Vercel: a vercel.json with a rewrite of /(.*) to /index.html. Firebase Hosting: "rewrites": [{ "source": "**", "destination": "/index.html" }]. If you cannot configure rewrites at all, HashRouter puts the route after a # (/#/courses), which the server never sees; the URLs look less nice but need no server setup.

Also set caching headers wisely: files in assets/ have hashed names, so they can be cached for a year; index.html must not be cached for long, or visitors will miss new releases.

Where to deploy

Because the output is static, you have many options:

Option How it works Good for
Shared hosting / cPanel upload the contents of dist into public_html (or a sub-folder) with the file manager, FTP or Git sites on a hosting plan you already pay for
Netlify, Vercel, Cloudflare Pages connect the Git repository; they run npm run build and publish dist on every push, with preview URLs for branches the easiest workflow, with a free tier
GitHub Pages publish dist to a branch or with a workflow; set base for project pages free hosting for small projects
A VPS / your own Nginx or Apache copy dist to the web root; configure rewrites and HTTPS full control
A CDN bucket (S3 + CloudFront, Firebase) upload files to storage behind a CDN high traffic, global speed

For the shared-hosting route, the steps are: run npm run build; open dist; upload everything inside it (not the folder itself) to the web root; add the rewrite rule; visit the site and a deep link to test. The article on deploying from Git to shared hosting shows a repeatable Git-based way to do it, including keeping secrets out of the repository.

A deployment checklist

  1. npm run lint and npm test pass.
  2. npm run build completes with no errors, and the sizes look reasonable.
  3. npm run preview: click through the main flows, reload on a deep URL.
  4. Environment variables for production are set (no secrets in VITE_ ones).
  5. base matches where the site is hosted.
  6. The server rewrites unknown paths to index.html and serves HTTPS.
  7. Update <title>, favicon and meta description in index.html.
  8. After deploying, open the live site in a private window, check the console for errors, and test on a phone.
  9. Keep the previous build around (or use Git tags) so you can roll back quickly.

Limits of a single-page app

Search engines and social previews see the initial HTML, which is nearly empty until JavaScript runs. Google does run JavaScript, but other crawlers and link previews often do not, and a blank first paint can be slow on weak devices. If search traffic or fast first load matters (shops, blogs, marketing pages), look at server rendering or static generation with a framework such as Next.js, or React Router in framework mode. Everything you have learned in this course (components, state, hooks) carries over unchanged.

Common mistakes

  • Uploading the dist folder itself instead of its contents, so URLs gain an extra /dist/.
  • Forgetting the rewrite rule, then getting 404 errors on refresh.
  • Putting secrets in VITE_ variables.
  • Deploying without a base setting when the site is not at the domain root, leaving a blank page and 404s for the JavaScript file.
  • Not testing the production build locally.
  • Caching index.html aggressively, so users keep an old version.
  • Editing code in dist or on the server instead of rebuilding.

Exercise

  1. Add a .env with VITE_APP_NAME=My App, show it in App, build, and confirm that the value appears in the built JavaScript file (search dist/assets).
  2. Set base: '/practice/', rebuild, and compare the paths in dist/index.html. Then serve dist from a sub-folder on any local web server to prove it works.
  3. Write the .htaccess rule for your router app, deploy it to any host (or a local Apache), and test refreshing on a deep URL.
  4. Connect a Git repository to Netlify, Vercel or Cloudflare Pages and watch the automatic deployment after a push.

Key points

  • npm run build compiles, bundles and minifies your app into the dist folder: plain static files that any web server can host.
  • Variables in .env files that start with VITE_ are copied into the code and are public; never put secrets in a React app.
  • Set base in vite.config.js when the site lives in a sub-folder, and configure the server to return index.html for unknown paths so client-side routes work on refresh.
  • Preview the build locally with npm run preview, then upload the contents of dist to your host or let a service build it from Git.

Check yourself

1. What do you upload to a static web host after npm run build?
2. Which environment variables can be read in React code with Vite?
3. Why must a secret API key never go in React environment variables?
4. A user refreshes https://example.com/courses/react and gets a 404 from the server. What is the fix?