Toolmingo
Guides9 min read

Vite Cheat Sheet: Config, Plugins, and Build Tricks

A complete Vite cheat sheet — vite.config.ts, dev server, build options, plugins, environment variables, path aliases, and common patterns. Copy-ready examples.

Vite is the fast, modern build tool that powers Vue, React, Svelte, and SvelteKit projects. This reference covers everything from vite.config.ts setup to production build optimisation.

Quick reference

The 25 Vite patterns you'll use every day.

Pattern What it does
npm create vite@latest Scaffold a new project
vite / vite dev Start dev server
vite build Production build
vite preview Preview production build locally
vite --port 3000 Custom port
vite --host Expose to LAN
defineConfig({}) Type-safe config helper
resolve.alias Path aliases (@/src/)
server.proxy Proxy API requests
build.outDir Change output directory
build.rollupOptions Custom Rollup options
build.sourcemap Enable source maps
build.minify Minifier (esbuild/terser)
plugins: [react()] Add plugin
import.meta.env.VITE_X Read env variable
import.meta.hot HMR API
?raw suffix Import as string
?url suffix Import as URL
?worker suffix Import as Web Worker
?inline suffix Inline as base64
optimizeDeps.include Force pre-bundle a dep
optimizeDeps.exclude Skip pre-bundling
css.modules CSS Modules config
assetsInclude Treat extra file types as assets
envPrefix Change env variable prefix

Installation and scaffolding

# Interactive scaffold (choose React/Vue/Svelte/Lit/Vanilla + JS/TS)
npm create vite@latest my-app
cd my-app && npm install && npm run dev

# Direct template flags
npm create vite@latest my-app -- --template react-ts
npm create vite@latest my-app -- --template vue-ts
npm create vite@latest my-app -- --template svelte-ts
npm create vite@latest my-app -- --template vanilla-ts

# Vite CLI
npm run dev       # vite
npm run build     # vite build
npm run preview   # vite preview

vite.config.ts structure

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'

export default defineConfig({
  plugins: [react()],

  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@lib': resolve(__dirname, 'src/lib'),
    },
  },

  server: {
    port: 5173,
    host: true,          // expose to LAN
    open: true,          // open browser on start
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },

  build: {
    outDir: 'dist',
    sourcemap: true,
    minify: 'esbuild',   // 'terser' for smaller output
    target: 'es2020',
  },
})

Path aliases

// vite.config.ts
import { resolve } from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
})
// tsconfig.json — must match vite.config.ts aliases
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
// usage in your code
import { Button } from '@/components/Button'
import { formatDate } from '@/lib/utils'

Environment variables

Vite exposes variables prefixed with VITE_ to the client.

# .env (shared, committed)
VITE_APP_TITLE=My App
VITE_API_URL=https://api.example.com

# .env.local (private, git-ignored)
VITE_SECRET_KEY=dev-only-secret

# .env.production
VITE_API_URL=https://api.production.com

# .env.development
VITE_API_URL=http://localhost:3001
// Access in code
const title = import.meta.env.VITE_APP_TITLE
const apiUrl = import.meta.env.VITE_API_URL
const mode = import.meta.env.MODE          // 'development' | 'production'
const isProd = import.meta.env.PROD        // boolean
const isDev = import.meta.env.DEV          // boolean

// TypeScript autocomplete — create env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_API_URL: string
}
interface ImportMeta {
  readonly env: ImportMetaEnv
}

Never put secrets in VITE_ variables — they end up in the client bundle.


Dev server configuration

export default defineConfig({
  server: {
    port: 3000,
    strictPort: true,      // fail if port is in use
    host: '0.0.0.0',       // expose on all interfaces
    open: '/dashboard',    // open specific path on start
    https: {},             // enable HTTPS (self-signed cert)
    cors: true,            // enable CORS for dev server

    proxy: {
      // Simple string shorthand
      '/api': 'http://localhost:3001',

      // Full options
      '/auth': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        secure: false,
        rewrite: (path) => path.replace(/^\/auth/, '/v1/auth'),
      },

      // WebSocket proxy
      '/ws': {
        target: 'ws://localhost:3001',
        ws: true,
      },
    },

    watch: {
      ignored: ['**/node_modules/**', '**/dist/**'],
    },
  },
})

Build configuration

export default defineConfig({
  build: {
    outDir: 'dist',
    assetsDir: 'assets',       // subdirectory for assets
    assetsInlineLimit: 4096,   // inline assets < 4KB as base64
    sourcemap: true,           // 'inline' | 'hidden' | boolean
    minify: 'esbuild',         // 'terser' = smaller, 'esbuild' = faster
    target: 'es2020',          // transpile target
    cssMinify: true,
    reportCompressedSize: true,

    // Code splitting
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
          ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
        },
      },
    },

    // Library mode (publishing a package)
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyLib',
      formats: ['es', 'cjs'],
      fileName: (format) => `my-lib.${format}.js`,
    },
  },
})

Plugins

Official plugins

npm i -D @vitejs/plugin-react          # React + Fast Refresh
npm i -D @vitejs/plugin-react-swc      # React + SWC (faster)
npm i -D @vitejs/plugin-vue            # Vue 3 SFCs
npm i -D @vitejs/plugin-vue-jsx        # Vue 3 JSX
npm i -D @vitejs/plugin-legacy         # Legacy browser support
import react from '@vitejs/plugin-react'
import legacy from '@vitejs/plugin-legacy'

export default defineConfig({
  plugins: [
    react(),
    legacy({
      targets: ['defaults', 'not IE 11'],
    }),
  ],
})

Community plugins

npm i -D vite-plugin-svgr              # Import SVGs as React components
npm i -D vite-tsconfig-paths           # Auto path aliases from tsconfig.json
npm i -D @vitejs/plugin-basic-ssl      # HTTPS with one line
npm i -D unplugin-icons                # 150k+ icons on demand
npm i -D vite-plugin-checker           # TypeScript/ESLint errors in overlay
import svgr from 'vite-plugin-svgr'
import tsconfigPaths from 'vite-tsconfig-paths'
import checker from 'vite-plugin-checker'

export default defineConfig({
  plugins: [
    react(),
    svgr(),                           // import Logo from './logo.svg?react'
    tsconfigPaths(),                  // reads tsconfig paths automatically
    checker({ typescript: true }),    // shows TS errors in browser overlay
  ],
})

Static assets

// Import as URL (hashed, copied to dist)
import imgUrl from './logo.png'
// → imgUrl = '/assets/logo-abc123.png'

// Import as raw string
import svgString from './icon.svg?raw'

// Import as inline base64
import inlineImg from './small.png?inline'

// Import as Web Worker
import MyWorker from './worker.ts?worker'
const worker = new MyWorker()

// Import as Shared Worker
import SharedWorker from './shared.ts?sharedworker'

// Import as URL without bundling
import wasmUrl from './module.wasm?url'
// vite.config.ts — treat extra extensions as assets
export default defineConfig({
  assetsInclude: ['**/*.gltf', '**/*.wasm'],
})

CSS

export default defineConfig({
  css: {
    // CSS Modules options
    modules: {
      localsConvention: 'camelCase',   // 'camelCase' | 'dashes' | 'camelCaseOnly'
      scopeBehaviour: 'local',
      generateScopedName: '[name]__[local]___[hash:base64:5]',
    },

    // PostCSS config (or put in postcss.config.js)
    postcss: {
      plugins: [
        // autoprefixer(), tailwindcss(), etc.
      ],
    },

    // Sass/Less/Stylus preprocessor options
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/variables" as *;`,
      },
    },

    devSourcemap: true,
  },
})
// CSS Modules usage
import styles from './Button.module.css'
<button className={styles.primary}>Click</button>

// Tailwind CSS — just install and add to postcss
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Hot Module Replacement (HMR)

// Manual HMR API (for non-framework code)
if (import.meta.hot) {
  import.meta.hot.accept('./dep.ts', (newModule) => {
    // handle updated module
    if (newModule) {
      newModule.doSomething()
    }
  })

  // Accept self-updates
  import.meta.hot.accept(() => {
    // re-run module
  })

  // Dispose side effects
  import.meta.hot.dispose((data) => {
    data.savedState = currentState
    clearInterval(timer)
  })

  // Restore state after hot reload
  const state = import.meta.hot.data.savedState ?? initialState
}

Optimising dependencies

export default defineConfig({
  optimizeDeps: {
    // Force pre-bundle (useful for CJS packages or monorepos)
    include: [
      'lodash-es',
      'date-fns',
      '@org/shared-pkg > internal-dep',
    ],

    // Skip pre-bundling (e.g. already ESM, or has issues)
    exclude: ['some-esm-package'],

    // Keep pre-bundled even when unused
    holdUntilCrawlEnd: false,
  },
})

Multi-page app (MPA)

import { resolve } from 'path'

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        about: resolve(__dirname, 'about/index.html'),
        dashboard: resolve(__dirname, 'dashboard/index.html'),
      },
    },
  },
})

Common patterns

Conditional config by mode

export default defineConfig(({ command, mode }) => {
  const isDev = command === 'serve'
  const isProd = command === 'build'
  const isTest = mode === 'test'

  return {
    plugins: [
      react(),
      isProd && someProdOnlyPlugin(),
    ].filter(Boolean),

    build: {
      sourcemap: isDev,
    },
  }
})

Environment-aware API base URL

// src/lib/api.ts
const BASE_URL = import.meta.env.VITE_API_URL ?? '/api'

export async function fetchUser(id: string) {
  const res = await fetch(`${BASE_URL}/users/${id}`)
  if (!res.ok) throw new Error(res.statusText)
  return res.json()
}

Loading .env variables in vite.config.ts itself

import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
  // Load env file from project root
  const env = loadEnv(mode, process.cwd(), '')

  return {
    define: {
      __APP_VERSION__: JSON.stringify(env.npm_package_version),
    },
  }
})

Global constants with define

export default defineConfig({
  define: {
    __APP_VERSION__: JSON.stringify('1.2.0'),
    __FEATURE_FLAGS__: JSON.stringify({ darkMode: true }),
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
  },
})

Common mistakes

Mistake Fix
Putting secrets in VITE_ env vars Use server-side env vars; VITE_ is public
Using __dirname without config import { resolve } from 'path' + resolve(__dirname, …) in config
Forgetting tsconfig.json paths when adding alias Alias in vite.config.ts and paths in tsconfig.json must match
optimizeDeps not picking up a CJS dep Add it to optimizeDeps.include
CSS not scoped in production Use .module.css or a CSS-in-JS solution
HMR full reload instead of partial Ensure module uses import.meta.hot.accept
Build output too large Add manualChunks in rollupOptions.output
.env.local committed to git Add .env*.local to .gitignore

Vite vs alternatives

Feature Vite webpack Parcel esbuild
Dev server speed ⚡ Instant (ESM) 🐢 Bundle first 🐢 Bundle first ⚡ Fast
HMR speed ⚡ Module-level 🐢 Full bundle Medium Manual
Config complexity Low High Zero Low
Plugin ecosystem Growing Huge Small Small
Build output Rollup (optimised) webpack Parcel Single file
SSR support Yes Yes Limited Manual
TypeScript Native Via loader Native Native
CSS Modules Built-in Via loader Built-in Manual

FAQ

Why does Vite use esbuild for dev but Rollup for build?
esbuild is 10–100× faster than Rollup but has a less complete plugin system and output optimisation. Vite uses esbuild for instant dev server startup and Rollup for production builds where bundle quality matters more than speed.

How do I use Vite with an existing webpack project?
Incrementally: add Vite for new pages/packages while keeping webpack for existing ones. Most webpack concepts map to Vite — loaders become plugins, resolve.alias stays the same name. The main gotcha is CJS vs ESM — Vite requires ESM source.

What does ?url, ?raw, ?inline mean?
These are Vite's import query suffixes that control how an asset is processed. ?url gives you a URL string, ?raw gives you the file content as a string, ?inline inlines the file as a base64 data URL. Without a suffix, Vite processes the file normally (images become URLs, JS is bundled, etc.).

How do I fix "cannot find module" for a path alias in tests (Vitest)?
Add the same resolve.alias to your vitest.config.ts (or the test key in vite.config.ts). Vitest uses Vite's resolver, but it reads its own config separately unless you use defineConfig from vitest/config.

How do I deploy a Vite app?
Run vite build, then serve the dist/ folder as static files. For SPA routing, configure your server to return index.html for all 404s. On Netlify: add _redirects file with /* /index.html 200. On Vercel: add vercel.json with a rewrite rule.

Can Vite build a library (not an app)?
Yes — set build.lib in vite.config.ts with an entry point and output formats (es, cjs, umd). Set build.rollupOptions.external to exclude peer dependencies like React from the bundle. This is the standard approach for publishing npm packages with Vite.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools