Skip to content

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

ts
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:

ts
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:

ts
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:

js
// 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 env

Notice 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 pathInternal 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.

ts
// 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'
  }
}

Released under the MIT License.