Category: React Js

  • How to Migrate a MERN App from CRA to Vite (Step-by-Step Guide)

    How to Migrate a MERN App from CRA to Vite (Step-by-Step Guide)

    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.