How to Migrate a MERN App from CRA to Vite (Step-by-Step Guide)
Create React App is officially deprecated, and your build times are paying the price. This step-by-step guide walks you through migrating a MongoDB, Express, React, and Node.js (MERN) application from CRA to Vite — covering config, environment variables, proxies, and common gotchas.

If your MERN stack app still runs on Create React App (CRA), it’s time to migrate. CRA has been officially deprecated by the React team, hasn’t received a major update in years, and its Webpack-based build process is painfully slow compared to modern tooling. Vite, on the other hand, gives you near-instant hot module replacement (HMR), dramatically faster cold starts, and a leaner, more modern development experience.
In this guide, I’ll walk you through migrating a real MERN application (MongoDB, Express, React, Node.js) from CRA to Vite, step by step, with all the code you need.
Why Migrate from CRA to Vite?
Before diving in, here’s why this migration is worth your time:
- Speed: Vite uses native ES modules during development, so your dev server starts in milliseconds instead of seconds, and HMR updates are nearly instant even on large
- CRA is deprecated: The React team removed CRA from the official “Start a New React Project” docs, meaning it no longer receives active support.
- Smaller, faster production builds: Vite uses Rollup under the hood for optimized, tree-shaken builds.
- Better developer experience: Native TypeScript support, easier plugin configuration, and no more “ejecting” to customize your build.
Prerequisites
Before starting, make sure you have:
- Node.js 18+ installed
- A working CRA-based MERN app (with a
/clientor/frontendfolder for React and a separate/serveror/backendfor Express) - Your project under version control (create a new branch for this migration, you’ll want to compare against the original)
Step 1: Create a Migration Branch
git checkout -b migrate-to-vite
Always migrate on a separate branch. Build tooling changes can break things in subtle ways, and you want an easy rollback path.
Step 2: Remove CRA and Install Vite
Inside your React app’s folder (e.g., /client):
npm uninstall react-scripts
npm install vite @vitejs/plugin-react --save-dev
If you’re using TypeScript:
npm install @vitejs/plugin-react-swc --save-dev
Step 3: Move index.html
CRA keeps index.html inside public/. Vite expects it at the project root.
mv public/index.html ./index.html
Then update it, remove the %PUBLIC_URL% placeholders and add a module script pointing to your entry file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My MERN App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Note the %PUBLIC_URL% references are gone — with Vite, static assets in public/ are referenced from the root path (/) directly.
Step 4: Rename Your Entry File
Vite requires .jsx (or .tsx) extensions for files containing JSX. Rename your entry point:
mv src/index.js src/main.jsx
Do the same for any other .js file that contains JSX, Vite’s default config won’t parse JSX inside plain .js files.
Step 5: Create vite.config.js
At the root of your React app:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
// Forward API calls to your Express backend
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
},
},
},
build: {
outDir: 'build', // keep 'build' so your Express static-serving code doesn't break
},
});
That proxy block is essential for a MERN setup — it replaces CRA’s "proxy": "http://localhost:5000" line in package.json, letting your React dev server forward /api requests to your Express backend without CORS issues.
Step 6: Update Environment Variables
CRA required environment variables to be prefixed with REACT_APP_. Vite uses VITE_ instead, and you access them via import.meta.env instead of process.env.
Before (CRA):
const apiUrl = process.env.REACT_APP_API_URL;
After (Vite):
const apiUrl = import.meta.env.VITE_API_URL;
Update your .env file accordingly:
VITE_API_URL=http://localhost:5000/api
Search your entire codebase for process.env.REACT_APP_ and process.env.NODE_ENV, and replace them, NODE_ENV becomes import.meta.env.MODE in Vite.
Step 7: Fix Static Asset Imports
Vite handles static assets slightly differently than Webpack/CRA:
- SVGs imported as React components need the
vite-plugin-svgrpackage if you were usingReactComponentimports. - Assets in
public/are referenced with an absolute path (/logo.png), not%PUBLIC_URL%/logo.png.
npm install vite-plugin-svgr --save-dev
// vite.config.js
import svgr from 'vite-plugin-svgr';
export default defineConfig({
plugins: [react(), svgr()],
// ...
});
Step 8: Update package.json Scripts
Replace your CRA scripts:
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
Remove "proxy" from package.json — it’s now handled inside vite.config.js.
Step 9: Update Jest/Testing Setup (If Applicable)
CRA bundled Jest by default. Vite doesn’t, so if you have existing tests, migrate to Vitest, which shares Vite’s config and is nearly a drop-in replacement:
npm install vitest @testing-library/react @testing-library/jest-dom --save-dev
// vite.config.js
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
},
});
Step 10: Test Everything
Run the dev server and check off this list:
- App loads without console errors
- Hot reload works when you edit a component
- API calls to your Express backend succeed (check the
/apiproxy) - Environment variables resolve correctly
- Images, fonts, and SVGs render properly
- Production build (
npm run build) completes and serves correctly via Express
npm run dev
Then test a production build:
npm run build
npm run preview
Step 11: Update Express to Serve the Vite Build
If your Express server serves the React build as static files, no changes are needed as long as you kept outDir: 'build' in vite.config.js:
app.use(express.static(path.join(__dirname, '../client/build')));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../client/build', 'index.html'));
});
Common Migration Errors and Fixes
| Error | Cause | Fix |
| process is not defined | process is not defined | Replace with import.meta.env |
| Blank page on load | index.html not at project root, or wrong script path | Move index.html, verify src="/src/main.jsx" |
| JSX syntax error | .js file contains JSX | Rename to .jsx |
| CORS errors calling API | Missing proxy config | Add server.proxy in vite.config.js |
| SVG imports break | CRA’s SVGR behavior not replicated | Install and configure vite-plugin-svgr |
Final Thoughts
Migrating from CRA to Vite typically takes a few hours for a mid-sized MERN app, and the payoff is immediate: faster local development, quicker CI builds, and a tool that’s actively maintained. Since CRA is no longer recommended by the React team, this isn’t just a performance upgrade, it’s a necessary move for any MERN project you plan to maintain long-term.
If you’re working on a larger app with many components, migrate incrementally: get the dev server running first, then tackle environment variables, then static assets, then testing, committing after each working stage.
Have questions about migrating your own MERN project, or want help auditing your app before the switch? Get in touch through hasnainalam.com.