React Js

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.

MERN stack app migration diagram showing Create React App to Vite transition

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 /client or /frontend folder for React and a separate /server or /backend for 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-svgr package if you were using ReactComponent imports.
  • 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 /api proxy)
  • 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

ErrorCauseFix
process is not definedprocess is not definedReplace with import.meta.env
Blank page on loadindex.html not at project root, or wrong script pathMove index.html, verify src="/src/main.jsx"
JSX syntax error.js file contains JSXRename to .jsx
CORS errors calling APIMissing proxy configAdd server.proxy in vite.config.js
SVG imports breakCRA’s SVGR behavior not replicatedInstall 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.