Configuring Vite
The four config options you reach for most: path aliases, the base path, the dev proxy, and the plugins worth adding.
defineConfig(({ mode }) => ({ base, plugins, resolve: { alias }, server: { proxy } }))vite.config.ts exports a config object, usually wrapped in defineConfig for types. Pass a function instead of an object when a value depends on the mode or the command, and the function receives { command, mode, isSsrBuild, isPreview }.
Aliases
resolve.alias maps an import prefix to a path, so @/components/Button.vue works from any depth. Values must be absolute, which is why the scaffold writes fileURLToPath(new URL('./src', import.meta.url)). An alias that Vite knows about but TypeScript does not will build and then fail vue-tsc, so add a matching entry to compilerOptions.paths.
The base path
base is the public path the app is served from and defaults to /. Set it to /app/ for a subdirectory deploy, or to a full URL when assets live on a CDN. ./ produces relative paths for a bundle opened from the filesystem. The value also appears as import.meta.env.BASE_URL, which is what the router and your own links should use rather than a hardcoded prefix.
The dev proxy
server.proxy forwards matching dev-server requests to another origin, so the browser sees one origin and CORS never comes up. It affects the dev server only, never the built output.
Plugins
@vitejs/plugin-vue compiles single-file components and is mandatory. vite-plugin-vue-devtools adds an in-page inspector. @vitejs/plugin-vue-jsx enables JSX in this project.
Example
<script setup lang="ts">
import { computed } from 'vue'
// Resolved by the '@' alias; '../../components/AppIcon.vue' is the same file.
import AppIcon from '@/components/AppIcon.vue'
const props = defineProps<{
docSlug: string
}>()
// BASE_URL already ends with a slash, so a deploy under /app/ still works.
const manualUrl = computed<string>(
() => `${import.meta.env.BASE_URL}manual/${props.docSlug}.pdf`,
)
</script>
<template>
<a :href="manualUrl" class="manual-link" download>
<app-icon name="download" />
Download the manual
</a>
</template>
<style scoped>
.manual-link {
display: inline-flex;
gap: 0.5rem;
align-items: center;
}
</style>When to use it
- Replace fragile ../../ import chains with a single @ alias across the project.
- Ship the same build to a site served from example.com/app/ by setting base.
- Call a local backend from the dev server without configuring CORS on it.
- Let each developer target a different API host through an env variable read in the config.
More examples
A config with all four pieces
The function form is what makes the conditional base possible. Both proxy forms are shown deliberately: the string is exactly the object with only target set, so you can recognise either in someone else's config. The rewrite strips the /api prefix, which is what you want when the backend serves /users rather than /api/users.
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig(({ mode }) => ({
// Served from example.com/app/ in production, from the root in dev.
base: mode === 'production' ? '/app/' : '/',
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
// Must be absolute; a relative string will not resolve.
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
proxy: {
// String shorthand for { target: 'http://localhost:8080' }.
'/health': 'http://localhost:8080',
// Longhand, needed once you want rewrite or changeOrigin.
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
}))Teaching TypeScript about the alias
This is tsconfig.app.json from the scaffold. The paths entry has to mirror resolve.alias, because Vite and vue-tsc resolve imports independently. Add an alias to only one of the two files and you get the confusing pair of symptoms: the dev server works, the editor underlines the import, and CI fails at the type-check step.
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}Pointing the dev server at a container
Each developer points DEV_API_TARGET at their own backend in .env.local, and nobody edits the committed config. host: true is the piece people miss when testing on a real phone: without it the dev server binds to localhost only and the device times out.
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [vue()],
resolve: {
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
},
server: {
// Listen on every interface so a phone on the LAN can reach it.
host: true,
port: 5173,
proxy: {
'/api': {
target: env.DEV_API_TARGET ?? 'http://localhost:8080',
changeOrigin: true,
},
},
},
}
})
Discussion