Module 7: Real-World Usage
Building and Deploying to Production
What the production build produces, build-time environment variables, fixing 404s on refresh, and deploying to Vercel, Netlify or AWS.
An app that only runs on your laptop isn't done. The good news: deploying a Vite React app is one of the easier things in web development, because the result is just static files. No server running Node, no database to host. Anything that can serve files can serve your app.
The build#
npm run build
Vite bundles, minifies and splits your code into the dist/ folder.
dist/
├── index.html
└── assets/
├── index-4f8a1c.js hashed filenames
├── index-9d0c2e.css
└── Reports-9b2e10.js
The hash in each filename comes from the file's contents. If a file changes, so does its name, which means browsers and CDNs can cache those assets for a year and still pick up your new version immediately, because index.html points to the new names. It's a neat solution to the "why am I still seeing the old version" problem.
Before deploying, check the production build locally. It can behave differently from the dev server.
npm run preview
This serves dist/ at a local address. A surprising number of bugs only appear here: a missing environment variable, an import with the wrong capitalization, a route that 404s.
Environment variables at build time#
Vite replaces import.meta.env.VITE_* values during the build. They end up as literal strings in the JavaScript files.
# .env.production
VITE_API_URL=https://api.myapp.com
Two consequences:
- You must set variables on your hosting platform before building, because
.envfiles are usually git-ignored and don't exist in the deploy environment. - Anything in there is visible to every visitor. Open the deployed JS and search for it. Public keys are fine. Secret keys never go here.
If you need a secret (a payment provider's private key, a database password), the client can't hold it. It goes in a backend or serverless function that your React app calls.
The refresh problem#
You deploy, click around, everything works. Then you copy https://myapp.com/products/42 into a new tab and get a 404.
When you click a link, React Router changes the URL with JavaScript. No request goes to the server. But when you load that URL directly or refresh, the browser asks the server for a file at /products/42. There's no such file. Only index.html exists, and that's the thing that knows how to render the route.
The fix is a rewrite rule: tell the host to serve index.html for any path that isn't a real file.
GET /assets/index-4f8a1c.js → real file, serve it
GET /products/42 → no file → serve /index.html, status 200
React Router reads the URL and renders ProductDetail
Each host configures this differently.
Vercel#
- Push your project to GitHub.
- In Vercel, import the repository. It detects Vite, with build command
npm run buildand output folderdist. - Add environment variables in the project settings.
- Deploy. Every push to your main branch redeploys, and each pull request gets a preview URL.
For client-side routing, add a vercel.json at the project root:
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
Netlify#
Similar flow: connect the repo, set the build command to npm run build and the publish directory to dist. For the rewrite, add a file at public/_redirects (it gets copied to dist during the build):
/* /index.html 200
The 200 matters. It makes it a rewrite (the URL stays the same). Without it, you get a redirect.
You can also use a netlify.toml:
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
AWS#
You have a few levels of effort here.
AWS Amplify Hosting is the closest to Vercel or Netlify. Connect the repo, it builds and hosts, and you add a rewrite rule in its console that routes unknown paths to /index.html with a 200 status.
S3 plus CloudFront is the classic do-it-yourself setup, common in companies already on AWS:
- Create an S3 bucket and upload the contents of
dist/(aws s3 sync dist/ s3://your-bucket --delete). - Create a CloudFront distribution with the bucket as origin, using Origin Access Control so the bucket stays private.
- Set the default root object to
index.html. - Add custom error responses so that 403 and 404 errors return
/index.htmlwith HTTP status 200. That's the rewrite equivalent. - After each deploy, invalidate the cache for
/index.html(the hashed assets don't need it).
It's more steps, but cheap and scalable. Use Cache-Control headers wisely: long cache times for the hashed files in assets/, and short or no-cache for index.html, otherwise users won't see your updates.
Deploying under a subpath#
If your app lives at https://example.com/my-app/ rather than at the root (GitHub Pages project sites do this), the asset URLs and router need to know:
// vite.config.js
export default defineConfig({
base: "/my-app/",
plugins: [react()],
});
const router = createBrowserRouter(routes, { basename: "/my-app" });
Skipping base is the usual reason for a blank page with 404 errors for the JS files in the console.
A short pre-deploy checklist#
npm run buildpasses with no errors.npm run previewworks, and you've tried refreshing on a nested route.- Environment variables are set on the host, with no secrets in
VITE_ones. - The API allows requests from your production domain (CORS).
- There's a 404 route in the router.
- Console is clean, and the page has a proper
titleand favicon. - Tests and lint pass (we cover those next module), ideally in CI before deploy.
Where people get burned#
- 404 on refresh because no rewrite is configured.
- A blank page after deploying under a subpath without setting
base. - Missing environment variables on the host, so API URLs are
undefined. - Secrets in
VITE_variables. - Case-sensitivity bugs: it worked on macOS or Windows, breaks on Linux build servers.
- Caching
index.htmltoo aggressively and users stuck on an old version. - CORS errors, because the API only allows
localhost.
Practice#
- Run a production build and preview it locally. Navigate to a nested route and refresh. Does it work?
- Deploy a small project to Vercel or Netlify using the free tier. Add the rewrite rule and test refresh on a deep link.
- Add an environment variable, redeploy, and show its value on the page. Find it in the built JS file to see that it's public.
- Deploy to a subpath on purpose, break it by omitting
base, then fix it.