| File | Purpose |
|---|---|
.env.development |
Loaded by vite dev. Sets VITE_API_URL=/api (MSW intercepts). |
.env.production |
Loaded by vite build. Sets VITE_API_URL=/api for this workshop; in a real project this would point to a real API host. |
VITE_ is the required prefix. Any variable without it is stripped from
the client bundle at build time — Vite replaces import.meta.env.VITE_*
string literals during the ESBuild transform step, so no env vars are
evaluated at runtime; they are baked in.
src/api/projects.ts and src/api/tasks.ts now derive the base URL from
the env var:
const API_BASE = import.meta.env.VITE_API_URL || '/api';The fallback (|| '/api') is a safety net; in practice the env file
always provides the value.
Augmenting ImportMetaEnv gives TypeScript knowledge of custom vars:
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}Without this, import.meta.env.VITE_API_URL is typed as string | undefined (or any depending on TS config). The augmentation makes it
string, eliminates the need for a non-null assertion, and catches typos
at compile time.
build: {
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router', 'react-router-dom'],
query: ['@tanstack/react-query'],
redux: ['@reduxjs/toolkit', 'react-redux'],
},
},
},
},Vite's build pipeline:
- ESBuild transpiles TypeScript/JSX and performs dead-code elimination within each module (tree shaking within files).
- Rollup bundles the module graph, applies further tree shaking across the whole dependency graph, and splits output into chunks.
- Each output chunk gets a content hash in its filename
(
vendor-abc123.js), enabling long-lived CDN caching. When only app code changes, browsers re-use the cached vendor chunk.
Manual chunks vs automatic splitting:
Vite's default is to split on dynamic import() boundaries (lazy routes).
manualChunks additionally groups specific npm packages together,
preventing them from being inlined into the app chunk.
A single-page application uses the History API to change the URL without
full page loads. When a user navigates directly to /projects/proj-1 or
refreshes the page, the hosting server sees a request for a path that has
no physical file. Without a fallback rule the server returns 404.
The fix is a rewrite/redirect rule: serve index.html for every path,
then React Router takes over and renders the correct component.
Vercel (vercel.json):
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }rewrites keeps the URL in the browser address bar unchanged (transparent
proxy), which is what you want for an SPA.
Netlify (netlify.toml):
[[redirects]]
from = "/*"
to = "/index.html"
status = 200Status 200 means "serve this content but keep the original URL" — a
rewrite, not a redirect. A 301/302 would cause a redirect loop.
Netlify _redirects is a simpler alternative to netlify.toml for
projects that do not need the build configuration block.
For containerised deployments the same pattern applies in nginx.conf:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}try_files attempts to serve the requested file, then the directory, and
falls back to index.html.
| Concept | Detail |
|---|---|
| Build-time env replacement | import.meta.env.VITE_* → string literal in bundle |
| Tree shaking | Dead code removed by Rollup across the whole module graph |
| Content hashing | Filenames include a hash of file content for cache busting |
| Code splitting | Parallel chunk downloads; unchanged chunks stay cached |
| SPA fallback | All server paths rewrite to index.html; client router handles the URL |
| Source maps | dist/*.map files map minified code back to TypeScript source |