Configuring Vite

The four config options you reach for most: path aliases, the base path, the dev proxy, and the plugins worth adding.

SyntaxdefineConfig(({ 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

Example · vue
<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.

Example · typescript
// 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.

Example · json
{
  "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.

Example · typescript
// 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

  • Be the first to comment on this lesson.