Virtual Modules
vite-env exposes two virtual modules that you import like regular packages. Both modules export a single named export env — a frozen object of validated environment values.
Import paths
import { env } from 'virtual:env/client' // client-safe variables only
import { env } from 'virtual:env/server' // all variables (server + client)Use virtual:env/client in browser code. Use virtual:env/server in server-side code, API routes, or Node scripts.
The env object
The env export is the only export you need. It is a plain object containing all validated values for that module's scope:
import { env } from 'virtual:env/client'
const url = env.VITE_API_URL // string
const debug = env.VITE_DEBUG // boolean (coerced by Zod)A default export (export default env) is also available, but the named env export is preferred for clarity.
Read-only at runtime
The env object is wrapped in Object.freeze() before being exported. Attempting to assign to it at runtime throws a TypeError in strict mode and silently fails otherwise:
import { env } from 'virtual:env/client'
env.VITE_API_URL = 'https://other.example.com'
// TypeError: Cannot assign to read only property 'VITE_API_URL'This makes accidental mutation visible immediately rather than producing a subtle bug.
Generated module code
The plugin generates the module content at build time by serialising the validated data. Here is what the client module looks like for a typical schema:
// virtual:env/client (generated by @vite-env/core — do not edit)
export const env = Object.freeze({
VITE_API_URL: 'https://api.example.com',
VITE_APP_NAME: 'My App',
VITE_DEBUG: true,
VITE_LOG_LEVEL: 'info',
})
export default envNotice that VITE_DEBUG is true (a boolean), not "true" (a string). Zod coercion runs before serialisation, so the values in the module already have their final types.
Vite 8 / Rolldown compatibility
Virtual module resolvers return { code: string, moduleType: 'js' }. The moduleType: 'js' field is required for Vite 8 and its Rolldown-based bundler. Without it, Rolldown may misinterpret the code string and fail to process the module correctly.
Virtual module ID convention
Vite requires virtual module IDs to be prefixed with \0 internally. The plugin maps each public path to its internal ID:
| Public import path | Internal resolved ID |
|---|---|
virtual:env/client | \0virtual:env/client |
virtual:env/server | \0virtual:env/server |
This is a standard Vite convention. You always use the virtual:env/* form in your source code — the \0 prefix is an implementation detail handled by the plugin.
TypeScript declarations
The plugin generates a vite-env.d.ts file at your project root that declares both modules with accurate types derived from your Zod schema. This file is updated on every build, so your types stay in sync with your schema automatically.
// vite-env.d.ts (generated)
declare module 'virtual:env/client' {
export const env: {
VITE_API_URL: string
VITE_APP_NAME: string
VITE_DEBUG: boolean
VITE_LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error'
}
}