| | |

Vite 25 ⚡ Environment API — Client, server & edge runtime separation

⏱️ Estimated read: 14 min | |

The Environment API is the answer to a limitation that Vite carried for its first five major versions: the assumption that an application has at most two environments. In Vite 5 and earlier, a dev server had an implicit client environment and an optional ssr environment, and the module graph mixed both into a single structure. A transformed module was represented by an id and an ssr boolean, and every API that touched the module graph had to accept that boolean to know which environment it was operating on . That model worked for a browser app and a Node.js SSR server. It did not work for an app that also ran on an edge runtime like Cloudflare Workers, because the edge runtime has different constraints, different globals, and different module resolution rules than either the browser or Node.

Vite 6 formalized the concept of environments as a first-class configuration surface . The environments config option lets an application declare any number of environments—client, ssr, edge, rsc, or anything else—each with its own module graph, its own resolved configuration, its own plugin pipeline, and its own build output. During development, a single Vite dev server runs code in all of these environments concurrently, each connected to a dev runtime that matches its production target as closely as possible . The client environment runs in the browser, the SSR environment runs in Node (or a module runner), and the edge environment runs in something like miniflare for Cloudflare Workers.

Key point: The Environment API replaces the single ssr boolean with per-environment instances. Each environment has its own moduleGraph, transformRequest(), warmupRequest(), and configuration. The environments config option is the declaration surface, and this.environment in plugin hooks is the runtime access point. The API is in release candidate status, stable enough for frameworks to build on but not yet frozen.


Why the single-environment model was replaced

The mixed module graph problem. In Vite 5, server.moduleGraph contained modules from both the client and the SSR environments. The nodes were connected through clientImportedModules and ssrImportedModules lists, and the APIs accepted an ssr boolean to distinguish the two . This worked when there were exactly two environments. It broke when a third environment was added, because a boolean cannot represent three states. The Environment API solves this by giving each environment its own moduleGraph and its own transformRequest() method, so the caller does not pass an environment flag; it calls the method on the environment instance .

The runtime fidelity problem. A Node.js SSR environment and an edge environment have different constraints. The edge runtime may not have process, may not have Node.js built-ins, and may resolve packages through the edge or worker export condition. In Vite 5, the SSR environment was configured as a single target, and the edge case was either unsupported or handled through workarounds. The Environment API lets each environment declare its own resolve.conditions, its own noExternal list, and its own build target . The edge environment can resolve edge-specific package exports while the SSR environment resolves Node.js exports, and both run in the same dev server.

The plugin context problem. A plugin that runs in multiple environments needs to know which environment it is operating on. The this.environment context in plugin hooks provides that information. A transform hook can check this.environment.name and apply a different transformation for 'ssr' than for 'edge' . Without the Environment API, the plugin had to rely on the ssr boolean or on configuration checks, which were fragile and could not distinguish the edge case from the Node.js case.

The trade-off. The Environment API adds configuration surface. A simple SPA does not need it; the top-level config options still apply to the client environment implicitly, and the behavior is the same as Vite 5 . The complexity appears when the app has more than one environment, and the developer must decide which options are shared and which are per-environment. The environments object inherits the top-level config by default, but options like optimizeDeps only apply to the client environment because they do not work well for server environments . The inheritance rules are documented, but they require attention.


a. Configuring multiple environments

The environments object is the declaration surface. Each key is an environment name, and each value is an EnvironmentOptions object that inherits from the top-level config.

// vite.config.ts — client, ssr, and edge environments
import { defineConfig } from 'vite';

export default defineConfig({
  // The top-level options are shared defaults.
  build: {
    sourcemap: false,
  },
  environments: {
    // The client environment is configured implicitly by the top-level options.
    // It can be configured explicitly, but the recommendation is to keep
    // the top-level options so the client config does not change
    // when new environments are added.
    client: {},

    // The SSR environment runs in Node.js.
    ssr: {
      build: {
        outDir: 'dist/server',
        target: 'node20',
      },
      resolve: {
        // Node.js package exports.
        conditions: ['node', 'import'],
      },
    },

    // The edge environment runs in a worker runtime.
    edge: {
      resolve: {
        // Bundle all dependencies; the edge runtime does not have node_modules.
        noExternal: true,
        // Edge-specific package exports.
        conditions: ['edge', 'worker'],
      },
      build: {
        outDir: 'dist/edge',
        // The edge runtime is not Node.js.
        target: 'es2022',
      },
    },
  },
});

The ssr and edge environments inherit the build.sourcemap: false option from the top level. They override the outDir and the target because those options differ per environment. The resolve.conditions array is the mechanism that tells Vite which package export to use. A package with an edge export condition resolves to the edge-specific file in the edge environment and to the Node.js file in the SSR environment .

The client environment is configured implicitly by the top-level options. The optimizeDeps option, for example, only applies to the client environment because dependency pre-bundling is a browser optimization. The documentation marks these options as non-inherit, and a server environment that inherited them would behave incorrectly . The explicit environments.client key exists, but the recommendation is to leave it empty and use the top-level options so the client configuration does not drift when new environments are added.


b. Accessing environments in plugins and the dev server

The environment instances are accessible from the dev server and from the plugin context. The server.environments object contains the client, ssr, and any custom environments that were configured.

// Accessing environments from a dev server
import { createServer } from 'vite';

const server = await createServer();

// The client environment is the browser environment.
const clientEnv = server.environments.client;

// The ssr environment is the Node.js environment.
const ssrEnv = server.environments.ssr;

// Each environment has its own module graph.
console.log(ssrEnv.moduleGraph);

// Each environment has its own transform method.
await ssrEnv.transformRequest('/src/entry-server.ts');
// An environment-aware plugin
import type { Plugin } from 'vite';

export function edgePlugin(): Plugin {
  return {
    name: 'edge-plugin',
    transform(code, id) {
      // this.environment is the environment the hook is running in.
      if (this.environment.name === 'edge') {
        // Replace Node.js globals with edge-compatible alternatives.
        return code.replace(/process\.env\./g, 'globalThis.env.');
      }
      // Return null for the other environments.
      return null;
    },
  };
}

The this.environment context is available in the transform, load, resolveId, and hotUpdate hooks . A plugin that runs in all environments can branch on the name and apply different logic. A plugin that should only run in one environment can use apply or perEnvironmentPlugin() to skip the others. The hotUpdate hook receives the environment context as well, which means a plugin can send a full reload only to the client environment when an SSR-only module changes .

The server.hot API moved to server.client.environment.hot in the migration . The old server.moduleGraph and server.transformRequest() methods still work for backward compatibility, but they are planned for deprecation in a future major version. The environment instances are the forward-compatible API.


Complete Example Session

// ============================================
// PART 1: THE THREE-ENVIRONMENT CONFIGURATION
// ============================================

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    sourcemap: false,
  },
  environments: {
    // Client: the browser.
    client: {},

    // SSR: the Node.js server.
    ssr: {
      build: {
        outDir: 'dist/server',
        target: 'node20',
      },
      resolve: {
        conditions: ['node', 'import'],
      },
    },

    // Edge: the worker runtime.
    edge: {
      resolve: {
        noExternal: true,
        conditions: ['edge', 'worker'],
      },
      build: {
        outDir: 'dist/edge',
        target: 'es2022',
      },
    },
  },
});

// ============================================
// PART 2: THE ENVIRONMENT-AWARE PLUGIN
// ============================================

// The plugin checks this.environment.name in the transform hook.
export function envPlugin(): Plugin {
  return {
    name: 'env-plugin',
    transform(code, id) {
      if (this.environment.name === 'ssr') {
        // SSR-specific transformation.
        return { code: code.replace(/__TARGET__/g, 'node'), map: null };
      }
      if (this.environment.name === 'edge') {
        // Edge-specific transformation.
        return { code: code.replace(/__TARGET__/g, 'edge'), map: null };
      }
      // The client environment receives the original code.
      return null;
    },
  };
}

// ============================================
// PART 3: ACCESSING THE ENVIRONMENTS
// ============================================

// The dev server exposes the environments.
import { createServer } from 'vite';

const server = await createServer();

// Each environment has its own module graph.
const clientGraph = server.environments.client.moduleGraph;
const ssrGraph = server.environments.ssr.moduleGraph;
const edgeGraph = server.environments.edge.moduleGraph;

// Each environment has its own transform method.
await server.environments.ssr.transformRequest('/src/entry-server.ts');
await server.environments.edge.transformRequest('/src/entry-edge.ts');

// ============================================
// PART 4: THE BUILDER API
// ============================================

// The Builder API runs the build for all environments.
import { createBuilder } from 'vite';

const builder = await createBuilder();
await builder.buildApp();

// The build output goes to the per-environment outDir.
// dist/client — the browser assets.
// dist/server — the Node.js entry.
// dist/edge — the edge worker entry.

// ============================================
// PART 5: THE EDGE RESOLUTION
// ============================================

// A package with an edge export condition:
// package.json
// {
//   "exports": {
//     ".": {
//       "node": "./node.js",
//       "edge": "./edge.js"
//     }
//   }
// }

// The edge environment resolves to edge.js.
// The ssr environment resolves to node.js.
// The conditions array in each environment controls the resolution.

// ============================================
// PART 6: THE PER-ENVIRONMENT STATE IN PLUGINS
// ============================================

// A plugin that runs in multiple environments needs
// to key its state by the environment.
export function statefulPlugin(): Plugin {
  // The Map is keyed by the environment instance.
  const state = new Map();

  return {
    name: 'stateful-plugin',
    buildStart() {
      // Initialize the state for this environment.
      state.set(this.environment, { count: 0 });
    },
    transform(code) {
      const envState = state.get(this.environment);
      envState.count++;
      return null;
    },
  };
}

// The same plugin instance is used for all environments.
// The Map ensures the state does not leak between them.

// ============================================
// PART 7: THE HOTUPDATE HOOK
// ============================================

// The hotUpdate hook receives the environment context.
export function hmrPlugin(): Plugin {
  return {
    name: 'hmr-plugin',
    hotUpdate({ modules, timestamp }) {
      // Only handle the client environment.
      if (this.environment.name !== 'client') return;

      // Send a full reload for the client.
      this.environment.hot.send({ type: 'full-reload' });
      return [];
    },
  };
}

// ============================================
// PART 8: THE MIGRATION FROM server.moduleGraph
// ============================================

// The old API:
// server.moduleGraph.getModuleByUrl(url, ssr)
// server.transformRequest(url, { ssr })

// The new API:
// server.environments.ssr.moduleGraph.getModuleByUrl(url)
// server.environments.ssr.transformRequest(url)

// The ssr boolean is gone. The environment instance is the context.

// ============================================
// PART 9: THE NON-INHERIT OPTIONS
// ============================================

// optimizeDeps only applies to the client environment.
// A server environment that inherited it would pre-bundle dependencies
// in a way that does not work for the server.
//
// The top-level optimizeDeps is implicitly the client's.
// The server environments do not inherit it.
//
// The same applies to other browser-specific options.

// ============================================
// PART 10: THE VERIFICATION CHECKLIST
// ============================================

// [ ] The environments object declares the client, ssr, and edge environments.
// [ ] The top-level options are the shared defaults.
// [ ] The per-environment options override the defaults.
// [ ] The resolve.conditions are set for the edge environment.
// [ ] The noExternal is set for the edge environment.
// [ ] The plugin uses this.environment.name to branch.
// [ ] The plugin state is keyed by the environment instance.
// [ ] The hotUpdate hook checks the environment name.
// [ ] The build output goes to per-environment outDir.

The ten parts covered the configuration, the environment-aware plugin, the environment access, the Builder API, the edge resolution, the plugin state, the hotUpdate hook, the migration from server.moduleGraph, the non-inherit options, and the verification. The through-line is that the environment is the context, and every API that used to accept an ssr boolean now operates on the environment instance.


Quick Reference

The environments config

KeyPurposeTypical settings
clientBrowser environmentImplicit from top-level config
ssrNode.js servertarget: 'node20', conditions: ['node']
edgeWorker runtimenoExternal: true, conditions: ['edge', 'worker']

The environment instance

Property/MethodPurpose
environment.nameThe environment identifier
environment.moduleGraphThe environment’s module graph
environment.transformRequest(url)Transform a module for this environment
environment.warmupRequest(url)Pre-warm the module graph
environment.hotThe HMR channel

The plugin context

ContextAvailable inPurpose
this.environmenttransform, load, resolveId, hotUpdateThe current environment
this.environment.nameSameBranch on the environment

The migration

Old APINew API
server.moduleGraphenvironment.moduleGraph
server.transformRequest(url, ssr)environment.transformRequest(url)
server.warmupRequest(url, ssr)environment.warmupRequest(url)
server.hotserver.client.environment.hot

Best Practices

✅ Do This:

// Declare the environments explicitly
environments: { client: {}, ssr: { build: { target: 'node20' } } } // ✅

// Use resolve.conditions for edge-specific exports
edge: { resolve: { conditions: ['edge', 'worker'] } }              // ✅

// Use this.environment.name in plugin hooks
if (this.environment.name === 'edge') { /* ... */ }                // ✅

// Key plugin state by the environment
const state = new Map(); state.set(this.environment, data);        // ✅

❌ Don’t Do This:

// Put environment-specific options at the top level
build: { target: 'node20' } // wrong for the client                // ❌

// Share outDir between environments
ssr: { build: { outDir: 'dist' } } // collides with client         // ❌

// Assume optimizeDeps applies to server environments
// It is client-only.                                             // ❌

// Use the ssr boolean in new code
server.transformRequest(url, { ssr: true }) // deprecated          // ❌

Common Pitfalls

PitfallWhy It HappensFix
The edge environment resolves the Node.js packageconditions not setAdd ['edge', 'worker']
The build output collidesThe same outDir for two environmentsSet a per-environment outDir
The plugin state leaks between environmentsThe state is not keyed by the environmentUse a Map keyed by this.environment
The client config changes when adding an environmentThe client is configured explicitlyLeave environments.client empty
optimizeDeps is ignored in the SSR environmentIt is client-onlyDo not rely on it for server environments

Real-World Examples

1. Client environment

environments: { client: {} } // implicit from top-level

2. SSR environment

ssr: { build: { outDir: 'dist/server', target: 'node20' } }

3. Edge environment

edge: { resolve: { noExternal: true, conditions: ['edge'] } }

4. Environment-aware plugin

transform(code) { if (this.environment.name === 'edge') { /* ... */ } }

5. Environment access

server.environments.ssr.transformRequest('/src/entry.ts')

6. Plugin state

const state = new Map(); state.set(this.environment, { count: 0 });

7. Hot update

hotUpdate() { if (this.environment.name !== 'client') return; }

8. Builder API

const builder = await createBuilder(); await builder.buildApp();

9. Migration

server.environments.client.moduleGraph // was server.moduleGraph

10. Non-inherit

// optimizeDeps is client-only

Visual

The Three Environments

┌──────────────────────────────────────────────────────────────┐
│  CLIENT, SSR, AND EDGE                                       │
│                                                              │
│  Client environment                                         │
│  ┌─────────────────────┐                                    │
│  │ Browser             │                                    │
│  │ resolve: browser    │                                    │
│  │ outDir: dist/client │                                    │
│  └─────────────────────┘                                    │
│                                                              │
│  SSR environment                                             │
│  ┌─────────────────────┐                                    │
│  │ Node.js             │                                    │
│  │ resolve: node       │                                    │
│  │ outDir: dist/server │                                    │
│  └─────────────────────┘                                    │
│                                                              │
│  Edge environment                                            │
│  ┌─────────────────────┐                                    │
│  │ Worker runtime      │                                    │
│  │ resolve: edge       │                                    │
│  │ outDir: dist/edge   │                                    │
│  └─────────────────────┘                                    │
│                                                              │
│  Each has its own module graph and build output.            │
└──────────────────────────────────────────────────────────────┘

The Environment Instance

┌──────────────────────────────────────────────────────────────┐
│  WHAT EACH ENVIRONMENT OWNS                                  │
│                                                              │
│  server.environments.ssr                                    │
│  ┌─────────────────────┐                                    │
│  │ moduleGraph         │  Its own module graph              │
│  │ transformRequest()  │  Its own transform method          │
│  │ warmupRequest()     │  Its own warmup                    │
│  │ hot                 │  Its own HMR channel               │
│  │ config              │  Its own resolved config           │
│  └─────────────────────┘                                    │
│                                                              │
│  The ssr boolean is gone.                                   │
│  The environment instance is the context.                   │
└──────────────────────────────────────────────────────────────┘

The Plugin Branch

┌──────────────────────────────────────────────────────────────┐
│  this.environment IN A PLUGIN                                 │
│                                                              │
│  transform(code) {                                           │
│    if (this.environment.name === 'ssr') {                    │
│      // Node.js transformation                               │
│    }                                                         │
│    if (this.environment.name === 'edge') {                   │
│      // Edge transformation                                  │
│    }                                                         │
│    // Client receives the original code                     │
│  }                                                           │
│                                                              │
│  The same plugin runs in all environments.                   │
│  The environment name branches the logic.                    │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
IntroducedVite 6 (RC), stable in Vite 7
Config optionenvironments
Environment instancesserver.environments.client, .ssr, .edge
Plugin contextthis.environment
Migrationserver.moduleGraph → environment.moduleGraph
Non-inherit optionsoptimizeDeps (client-only)
Builder APIcreateBuilder().buildApp()

Key takeaways:

  • The Environment API replaces the single ssr boolean with per-environment instances. Each environment has its own module graph, its own transform method, and its own configuration. The environments config option is the declaration surface, and this.environment in plugin hooks is the runtime access point .
  • The edge runtime is the reason the API exists. Vite 5 could not properly represent a browser, a Node.js server, and an edge server in the same dev server. The Environment API lets each runtime declare its own resolve.conditions and noExternal list, so a package with edge-specific exports resolves correctly in the edge environment .
  • The top-level config is the shared default. A simple SPA does not need the environments object; the top-level options apply to the client environment implicitly. The environments object is for apps with more than one environment, and the per-environment options override the top-level defaults .
  • Plugin state must be keyed by the environment. The same plugin instance runs in multiple environments. A Map keyed by this.environment prevents the state from leaking between the client, SSR, and edge environments .
  • The migration is a move from server methods to environment methods. server.moduleGraph becomes environment.moduleGraph, and server.transformRequest(url, ssr) becomes environment.transformRequest(url). The old methods still work for backward compatibility, but they are planned for deprecation .

Remember: The Environment API is the formalization of a concept that Vite had been treating implicitly. The client and ssr environments were always there; the API makes them explicit and allows more of them. The edge environment is the case that forced the change, because a boolean cannot represent a worker runtime with its own package export conditions. The environment instance is the context, and the environments config is the declaration. The API is in release candidate status, stable enough for frameworks to build on, and the migration path from the old server methods is documented.

Was this helpful?


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!