Vite 11 ⚡ The vite.config.js File — Core configuration surface
The vite.config.js file is the single entry point for configuring Vite. When the vite command runs, it automatically resolves a config file named vite.config.js (or .ts, .mjs, .cjs) from the project root . The file exports a configuration object, and every option in that object controls one aspect of the build or the development server. The surface is deliberately flat: there is no plugin required to change the server port, add a path alias, or configure a proxy. The file is JavaScript, which means it can compute values, read environment variables, and return different configurations for development and production.
This chapter covers the core configuration surface: the defineConfig helper, the conditional and async forms, the root-level options that determine where the project lives (root, base, publicDir), the resolution options (resolve.alias, resolve.dedupe), the define replacements, and the server block that controls the development server. It also covers the Vite 8 migration that renamed rollupOptions to rolldownOptions and esbuild to oxc.
Key point: The config file can be an object, a function, or an async function. The function form receives the command ('serve' or 'build') and the mode, which is how a single config file produces different behavior for development and production.
Why the config file is JavaScript
The static problem. A JSON configuration file cannot compute values. It cannot read process.env, concatenate a path from __dirname, or return a different value for a different environment. The Vite config is JavaScript because the configuration often depends on the context: the port changes between the developer’s machine and CI, the alias resolves relative to the project directory, and the plugins differ between development and production.
The type problem. Vite ships with TypeScript typings. The defineConfig helper provides intellisense without JSDoc annotations, and a vite.config.ts file gets type checking for every option . The helper is a thin wrapper that returns the object unchanged, but its type signature is what makes the editor autocomplete the option names and flag invalid values.
The conditional problem. The defineConfig callback form receives the command and mode. A config that adds a bundle analyzer only for production returns a different object when command === 'build'. The callback is the mechanism that keeps the development and production configurations in one file without duplicating the shared options .
a. The defineConfig helper and the config forms
The defineConfig helper is imported from vite. The basic form is an object.
import { defineConfig } from 'vite'
export default defineConfig({
root: '.',
base: '/',
})
The conditional form is a function that receives the command, mode, isSsrBuild, and isPreview flags.
export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => {
if (command === 'serve') {
return { server: { port: 3000 } }
}
return { build: { outDir: 'dist' } }
})
The command is 'serve' during development and 'build' for production. The mode is the mode passed with --mode, which defaults to 'development' for serve and 'production' for build . The isSsrBuild and isPreview flags differentiate the kind of build and serve command, and the documentation recommends comparing them explicitly against true and false because some tools that load the config pass undefined .
The async form is a function that returns a Promise. It is used when the config needs to call an async function, such as reading a file or fetching a value before the config is resolved.
export default defineConfig(async ({ command, mode }) => {
const data = await asyncFunction()
return { define: { __DATA__: JSON.stringify(data) } }
})
b. The root-level options
The root option sets the project root, which is the directory where Vite looks for index.html and where the config file is resolved. The default is the current working directory .
The base option sets the public base path. It defaults to /. A project deployed to a subdirectory uses base: '/app/', and Vite prefixes every asset URL with that path.
The publicDir option sets the directory for static assets that are copied as-is. The default is public. Setting it to false disables the public directory entirely .
The cacheDir option sets the directory for Vite’s cache. The default is node_modules/.vite .
The plugins option is an array. Vite flattens nested arrays and ignores falsy values, which allows a plugin list to be composed conditionally .
c. Resolution and replacement
The resolve.alias option maps import specifiers to filesystem paths. The paths should be absolute for filesystem aliases.
resolve: {
alias: {
'@': '/src',
},
}
The resolve.dedupe option forces a single instance of a package. It is used when a dependency is duplicated in the tree and the duplicate causes a broken singleton .
The resolve.tsconfigPaths option, new in Vite 8, resolves the paths from tsconfig.json. It defaults to false, so a project that relied on automatic paths resolution must opt in .
The define option performs compile-time replacement. The values must be JSON-serializable or single identifiers, and non-strings are automatically wrapped with JSON.stringify .
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
__API_URL__: 'window.__backend_api_url',
}
d. The server block
The server option configures the development server. The host setting binds the server to all interfaces (true) or a specific address. The port sets the port, and strictPort makes Vite exit if the port is already in use instead of trying the next one .
server: {
host: true,
port: 5173,
strictPort: true,
open: true,
cors: true,
}
The server.proxy option forwards requests to a backend. The keys are the path prefixes, and the values are the target and options.
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}
The proxied requests are not transformed by Vite; they are forwarded as-is to the target . The server.cors option defaults to allowing localhost, and setting it to true allows any origin, which the documentation warns against .
The server.warmup option transforms and caches files in advance to improve the initial page load. The clientFiles and ssrFiles arrays list the files to warm up .
e. The Vite 8 migration
Vite 8 replaced the Rollup bundler with Rolldown and the esbuild transformer with Oxc. The old option names still work through a compatibility layer, but they are deprecated .
The build.rollupOptions option is renamed to build.rolldownOptions. The esbuild option is renamed to oxc, and optimizeDeps.esbuildOptions is renamed to optimizeDeps.rolldownOptions . The @vitejs/plugin-react v6 uses Oxc instead of Babel, and the babel option is removed .
Complete Example Session
// ============================================
// PART 1: THE BASIC CONFIG
// ============================================
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
root: '.',
base: '/',
plugins: [],
})
// ============================================
// PART 2: THE CONDITIONAL CONFIG
// ============================================
export default defineConfig(({ command, mode }) => {
if (command === 'serve') {
return { server: { port: 3000 } }
}
return { build: { outDir: 'dist' } }
})
// ============================================
// PART 3: THE ASYNC CONFIG
// ============================================
export default defineConfig(async ({ mode }) => {
const data = await fetchConfig(mode)
return { define: { __CONFIG__: JSON.stringify(data) } }
})
// ============================================
// PART 4: THE ROOT OPTIONS
// ============================================
export default defineConfig({
root: 'src',
base: '/app/',
publicDir: 'static',
cacheDir: '.vite-cache',
})
// ============================================
// PART 5: THE ALIAS
// ============================================
import { fileURLToPath } from 'node:url'
export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
// ============================================
// PART 6: THE DEDUPE
// ============================================
export default defineConfig({
resolve: {
dedupe: ['react', 'react-dom'],
},
})
// ============================================
// PART 7: THE DEFINE
// ============================================
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
__DEV__: 'process.env.NODE_ENV !== "production"',
},
})
// ============================================
// PART 8: THE SERVER BLOCK
// ============================================
export default defineConfig({
server: {
host: true,
port: 5173,
strictPort: true,
open: true,
},
})
// ============================================
// PART 9: THE PROXY
// ============================================
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
},
},
})
// ============================================
// PART 10: THE VITE 8 MIGRATION
// ============================================
// Deprecated → Replacement
// build.rollupOptions → build.rolldownOptions
// esbuild → oxc
// optimizeDeps.esbuildOptions → optimizeDeps.rolldownOptions
export default defineConfig({
build: {
rolldownOptions: {},
},
oxc: {},
})
The ten parts covered the basic config, the conditional form, the async form, the root options, the alias, the dedupe, the define, the server block, the proxy, and the Vite 8 migration.
Quick Reference
Config forms
| Form | Signature |
|---|---|
| Object | defineConfig({ ... }) |
| Conditional | defineConfig(({ command, mode }) => ({ ... })) |
| Async | defineConfig(async ({ mode }) => ({ ... })) |
Root-level options
| Option | Default | Purpose |
|---|---|---|
root | cwd | Project root, where index.html lives |
base | / | Public base path |
publicDir | public | Static assets copied as-is |
cacheDir | node_modules/.vite | Vite cache |
plugins | [] | Plugin array (flattened) |
Resolution
| Option | Purpose |
|---|---|
resolve.alias | Path mapping (absolute paths) |
resolve.dedupe | Force single package instance |
resolve.tsconfigPaths | Enable tsconfig paths (Vite 8, default false) |
Server
| Option | Default | Purpose |
|---|---|---|
host | localhost | Bind address |
port | 5173 | Port |
strictPort | false | Exit if port in use |
open | false | Open browser on start |
proxy | — | Backend proxy |
warmup | — | Pre-transform files |
Vite 8 renames
| Deprecated | Replacement |
|---|---|
build.rollupOptions | build.rolldownOptions |
esbuild | oxc |
optimizeDeps.esbuildOptions | optimizeDeps.rolldownOptions |
Best Practices
✅ Do This:
// Use defineConfig for intellisense
import { defineConfig } from 'vite'
export default defineConfig({ ... }) // ✅
// Use the conditional form for dev/build differences
defineConfig(({ command }) => command === 'serve' ? ... : ...) // ✅
// Use absolute paths for aliases
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } // ✅
// Use rolldownOptions in new code
build: { rolldownOptions: {} } // ✅
❌ Don’t Do This:
// Use rollupOptions in new code
build: { rollupOptions: {} } // ❌ (deprecated)
// Set cors: true in production
server: { cors: true } // ❌
// Rely on tsconfig paths without opting in
// resolve.tsconfigPaths defaults to false // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Alias resolves to a relative path | Used a relative string | Use an absolute path with fileURLToPath |
.env not available in config | Vite defers .env loading until after config resolution | Use loadEnv or process.env |
| Proxy does not forward WebSocket | ws: true not set | Add ws: true to the proxy entry |
| Config not found | File not at the project root | Use --config or move the file |
| Deprecated option warning | rollupOptions or esbuild used | Rename to rolldownOptions and oxc |
Real-World Examples
1. Basic config
export default defineConfig({ plugins: [] })
2. Conditional config
defineConfig(({ command }) => command === 'serve' ? { server: { port: 3000 } } : {})
3. Alias
resolve: { alias: { '@': '/src' } }
4. Dedupe
resolve: { dedupe: ['react'] }
5. Define
define: { __VERSION__: JSON.stringify('1.0.0') }
6. Server port
server: { port: 3000, strictPort: true }
7. Proxy
server: { proxy: { '/api': 'http://localhost:8080' } }
8. Warmup
server: { warmup: { clientFiles: ['./src/main.tsx'] } }
9. Vite 8 build
build: { rolldownOptions: {} }
10. Vite 8 transform
oxc: { jsx: { runtime: 'automatic' } }
Visual
The config forms
┌─────────────────────────────────────────────────────────────┐
│ OBJECT FUNCTION │
│ │
│ defineConfig({ ... }) defineConfig(({ command }) => │
│ command === 'serve' │
│ ? devConfig │
│ : buildConfig) │
└─────────────────────────────────────────────────────────────┘
The root options
┌─────────────────────────────────────────────────────────────┐
│ root ──► where index.html lives │
│ base ──► public URL prefix │
│ publicDir ──► static assets copied as-is │
│ cacheDir ──► Vite cache location │
└─────────────────────────────────────────────────────────────┘
The server block
┌─────────────────────────────────────────────────────────────┐
│ server: { │
│ host: true, ← bind all interfaces │
│ port: 5173, ← dev port │
│ proxy: { '/api': 'http://localhost:3000' } │
│ } │
└─────────────────────────────────────────────────────────────┘
The Vite 8 renames
┌─────────────────────────────────────────────────────────────┐
│ rollupOptions ──► rolldownOptions │
│ esbuild ──► oxc │
│ optimizeDeps.esbuildOptions ──► optimizeDeps.rolldownOptions│
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Default file | vite.config.js at project root |
| Helper | defineConfig |
| Conditional form | Receives command, mode, isSsrBuild, isPreview |
| Async form | Returns a Promise |
| Project root | root |
| Public path | base |
| Static assets | publicDir |
| Aliases | resolve.alias (absolute paths) |
| Compile-time constants | define |
| Dev server | server block |
| Vite 8 bundler | rolldownOptions |
| Vite 8 transformer | oxc |
Key takeaways:
- The config file is JavaScript, not JSON. It can compute values, read the environment, and return different configurations for development and production. The
defineConfighelper provides type checking and intellisense, and the callback form receives thecommandandmodethat determine which branch to return . - The root-level options determine where the project lives.
rootis the directory with theindex.html.baseis the public URL prefix.publicDiris the static assets directory.cacheDiris the Vite cache location . - The resolution options control how imports are resolved.
resolve.aliasmaps specifiers to absolute paths.resolve.dedupeforces a single instance of a package.resolve.tsconfigPathsopts intotsconfigpathsresolution, which defaults tofalsein Vite 8 . - The
defineoption performs compile-time replacement. The values must be JSON-serializable or single identifiers. It is the mechanism for injecting build-time constants like a version string or an API URL . - The
serverblock controls the development server. Thehost,port,strictPort,open, andproxyoptions are the most commonly used. The proxy forwards requests to a backend without Vite transforming them . - Vite 8 renamed the bundler and transformer options.
build.rollupOptionsbecomesbuild.rolldownOptions, andesbuildbecomesoxc. The old names still work through a compatibility layer, but new code should use the new names .
Remember: The vite.config.js file is the single place where the build and the development server are configured. The defineConfig helper is the recommended entry point because it provides type checking. The callback form is the mechanism for conditional configuration. The root-level options, the resolution options, the define replacements, and the server block are the surface that most projects touch. And the Vite 8 migration renamed rollupOptions to rolldownOptions and esbuild to oxc; use the new names in new code.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!