How to Use Environment Variables
Learn how to configure and use environment variables in your TanStack Router application for API endpoints, feature flags, and build configuration across different bundlers.
Quick Start#
Environment variables in TanStack Router are primarily used for client-side configuration and must follow bundler-specific naming conventions for security.
# .env
VITE_API_URL=https://api.example.com
VITE_ENABLE_DEVTOOLS=true// Route configuration
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: async () => {
const apiUrl = import.meta.env.VITE_API_URL
const response = await fetch(`${apiUrl}/posts`)
return response.json()
},
component: PostsList,
})Environment Variable Access Patterns#
Vite-Based Projects (Most Common)#
With Vite, environment variables must be prefixed with VITE_ to be accessible in client code:
// Route loaders
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const apiUrl = import.meta.env.VITE_API_URL // ✅ Works
const apiKey = import.meta.env.VITE_PUBLIC_API_KEY // ✅ Works
// This would be undefined (security feature):
// const secret = import.meta.env.SECRET_KEY // ❌ Undefined
return fetchDashboardData(apiUrl, apiKey)
},
})
// Components
export function ApiStatus() {
const isDev = import.meta.env.DEV // ✅ Built-in Vite variable
const isProd = import.meta.env.PROD // ✅ Built-in Vite variable
const mode = import.meta.env.MODE // ✅ development/production
return (
<div>
Environment: {mode}
{isDev && <DevToolsPanel />}
</div>
)
}Webpack-Based Projects#
Configure webpack's DefinePlugin to inject environment variables. Note: Webpack doesn't support import.meta.env by default, so use process.env patterns:
// webpack.config.js
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.ENABLE_FEATURE': JSON.stringify(process.env.ENABLE_FEATURE),
}),
],
}
// Usage in routes
export const Route = createFileRoute('/api-data')({
loader: async () => {
const response = await fetch(`${process.env.API_URL}/data`)
return response.json()
},
component: () => {
const enableFeature = process.env.ENABLE_FEATURE === 'true'
return enableFeature ? <NewFeature /> : <OldFeature />
},
})Rspack-Based Projects#
Rspack uses the PUBLIC_ prefix convention. Note: import.meta.env support depends on your Rspack configuration and runtime - you may need to configure builtins.define properly:
# .env
PUBLIC_API_URL=https://api.example.com
PUBLIC_FEATURE_FLAG=true// Route usage
export const Route = createFileRoute('/features')({
loader: async () => {
const apiUrl = import.meta.env.PUBLIC_API_URL
return fetch(`${apiUrl}/features`).then(r => r.json())
},
component: () => {
const enableFeature = import.meta.env.PUBLIC_FEATURE_FLAG === 'true'
return enableFeature ? <NewFeature /> : <OldFeature />
},
})ESBuild Projects#
Configure defines manually:
// build script
import { build } from 'esbuild'
await build({
entryPoints: ['src/main.tsx'],
define: {
'process.env.NODE_ENV': '"production"',
'process.env.API_URL': `"${process.env.API_URL}"`,
},
})Common Patterns#
API Configuration in Route Loaders#
// src/routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router'
const fetchPosts = async () => {
const baseUrl = import.meta.env.VITE_API_URL
const apiKey = import.meta.env.VITE_API_KEY
const response = await fetch(`${baseUrl}/posts`, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error('Failed to fetch posts')
}
return response.json()
}
export const Route = createFileRoute('/posts/')({
loader: fetchPosts,
errorComponent: ({ error }) => (
<div>Error loading posts: {error instanceof Error ? error.message : String(error)}</div>
),
})Environment-Based Route Configuration#
// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<Outlet />
{/* Only show devtools in development */}
{import.meta.env.DEV && <TanStackRouterDevtools />}
</>
),
})Feature Flags in Routes#
// src/lib/features.ts
export const features = {
enableNewDashboard: import.meta.env.VITE_ENABLE_NEW_DASHBOARD === 'true',
enableAnalytics: import.meta.env.VITE_ENABLE_ANALYTICS === 'true',
debugMode: import.meta.env.DEV,
}
// src/routes/dashboard/index.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { features } from '../../lib/features'
export const Route = createFileRoute('/dashboard/')({
beforeLoad: () => {
// Redirect to old dashboard if new one is disabled
if (!features.enableNewDashboard) {
throw redirect({ to: '/dashboard/legacy' })
}
},
component: NewDashboard,
})Authentication Configuration#
// src/lib/auth.ts
export const authConfig = {
domain: import.meta.env.VITE_AUTH0_DOMAIN,
clientId: import.meta.env.VITE_AUTH0_CLIENT_ID,
redirectUri: `${window.location.origin}/callback`,
}
// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { authConfig } from '../lib/auth'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
const isAuthenticated = await checkAuthStatus()
if (!isAuthenticated) {
// Redirect to auth provider
const authUrl = `https://${authConfig.domain}/authorize?client_id=${authConfig.clientId}&redirect_uri=${authConfig.redirectUri}`
window.location.href = authUrl
return
}
},
})Search Params with Environment Config#
// src/routes/search.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const searchSchema = z.object({
q: z.string().optional(),
category: z.string().optional(),
})
export const Route = createFileRoute('/search')({
validateSearch: searchSchema,
loader: async ({ search }) => {
const apiUrl = import.meta.env.VITE_SEARCH_API_URL
const params = new URLSearchParams({
q: search.q || '',
category: search.category || 'all',
api_key: import.meta.env.VITE_SEARCH_API_KEY,
})
const response = await fetch(`${apiUrl}/search?${params}`)
return response.json()
},
})Environment File Setup#
File Hierarchy (Vite)#
Vite loads environment files in this order:
.env.local # Local overrides (add to .gitignore)
.env.production # Production-specific
.env.development # Development-specific
.env # Default (commit to git)Example Configuration#
.env (committed to repository):
# API Configuration
VITE_API_URL=https://api.example.com
VITE_API_VERSION=v1
# Feature Flags
VITE_ENABLE_NEW_UI=false
VITE_ENABLE_ANALYTICS=true
# Auth Configuration (public keys only)
VITE_AUTH0_DOMAIN=your-domain.auth0.com
VITE_AUTH0_CLIENT_ID=your-client-id
# Build Configuration
VITE_APP_NAME=TanStack Router App
VITE_APP_VERSION=1.0.0.env.local (add to .gitignore):
# Development overrides
VITE_API_URL=http://localhost:3001
VITE_ENABLE_NEW_UI=true
VITE_DEBUG_MODE=true.env.production:
# Production-specific
VITE_API_URL=https://api.prod.example.com
VITE_ENABLE_ANALYTICS=true
VITE_ENABLE_NEW_UI=trueType Safety#
Vite TypeScript Declarations#
Create src/vite-env.d.ts:
/// <reference types="vite/client" />
interface ImportMetaEnv {
// API Configuration
readonly VITE_API_URL: string
readonly VITE_API_VERSION: string
readonly VITE_API_KEY?: string
// Feature Flags
readonly VITE_ENABLE_NEW_UI: string
readonly VITE_ENABLE_ANALYTICS: string
readonly VITE_DEBUG_MODE?: string
// Authentication
readonly VITE_AUTH0_DOMAIN: string
readonly VITE_AUTH0_CLIENT_ID: string
// App Configuration
readonly VITE_APP_NAME: string
readonly VITE_APP_VERSION: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}Runtime Validation#
Use Zod to validate environment variables at startup with fallbacks and optional values:
// src/config/env.ts
import { z } from 'zod'
const envSchema = z.object({
// Required variables
VITE_API_URL: z.string().url(),
VITE_AUTH0_DOMAIN: z.string(),
VITE_AUTH0_CLIENT_ID: z.string(),
VITE_APP_NAME: z.string(),
// Optional with defaults
VITE_API_VERSION: z.string().default('v1'),
VITE_ENABLE_NEW_UI: z.string().default('false'),
VITE_ENABLE_ANALYTICS: z.string().default('true'),
// Optional variables
VITE_DEBUG_MODE: z.string().optional(),
VITE_SENTRY_DSN: z.string().optional(),
})
// Validate at app startup with fallbacks
export const env = envSchema.parse({
...import.meta.env,
// Provide fallbacks for missing optional values
VITE_API_VERSION: import.meta.env.VITE_API_VERSION || 'v1',
VITE_ENABLE_NEW_UI: import.meta.env.VITE_ENABLE_NEW_UI || 'false',
VITE_ENABLE_ANALYTICS: import.meta.env.VITE_ENABLE_ANALYTICS || 'true',
})
// Typed helper functions
export const isFeatureEnabled = (flag: keyof typeof env) => {
return env[flag] === 'true'
}
// Type-safe boolean conversion
export const getBooleanEnv = (
value: string | undefined,
defaultValue = false,
): boolean => {
if (value === undefined) return defaultValue
return value === 'true'
}Usage with Type Safety#
// src/routes/api-data.tsx
import { createFileRoute } from '@tanstack/react-router'
import { env, isFeatureEnabled } from '../config/env'
export const Route = createFileRoute('/api-data')({
loader: async () => {
// TypeScript knows these are strings and exist
const response = await fetch(`${env.VITE_API_URL}/${env.VITE_API_VERSION}/data`)
return response.json()
},
component: () => {
return (
<div>
<h1>{env.VITE_APP_NAME}</h1>
{isFeatureEnabled('VITE_ENABLE_NEW_UI') && <NewUIComponent />}
</div>
)
},
})Bundler-Specific Configuration#
Vite Configuration#
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
react(),
// tanstackRouter generates route tree and enables file-based routing
tanstackRouter(),
],
// Environment variables are handled automatically
// Custom environment variable handling:
define: {
// Global constants (these become available as global variables)
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
},
})Webpack Configuration#
// webpack.config.js
const { TanStackRouterWebpack } = require('@tanstack/router-webpack-plugin')
const webpack = require('webpack')
module.exports = {
plugins: [
// TanStackRouterWebpack generates route tree and enables file-based routing
new TanStackRouterWebpack(),
new webpack.DefinePlugin({
// Inject environment variables (use process.env for Webpack)
'process.env.API_URL': JSON.stringify(process.env.API_URL),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.ENABLE_FEATURE': JSON.stringify(process.env.ENABLE_FEATURE),
}),
],
}Rspack Configuration#
// rspack.config.js
const { TanStackRouterRspack } = require('@tanstack/router-rspack-plugin')
module.exports = {
plugins: [
// TanStackRouterRspack generates route tree and enables file-based routing
new TanStackRouterRspack(),
],
// Rspack automatically handles PUBLIC_ prefixed variables for import.meta.env
// Custom handling for additional variables:
builtins: {
define: {
// Define additional variables (these become global replacements)
'process.env.API_URL': JSON.stringify(process.env.PUBLIC_API_URL),
__BUILD_TIME__: JSON.stringify(new Date().toISOString()),
},
},
}Production Checklist#
- All client-exposed variables use appropriate prefix (VITE_, PUBLIC_, etc.)
- No sensitive data (API secrets, private keys) in environment variables
- .env.local is in .gitignore
- Production environment variables are configured on hosting platform
- Required environment variables are validated at build time
- TypeScript declarations are up to date
- Feature flags are properly configured for production
- API URLs point to production endpoints
Common Problems#
Environment Variable is Undefined#
Problem: import.meta.env.MY_VARIABLE returns undefined
Solutions:
- Add correct prefix: Use VITE_ for Vite, PUBLIC_ for Rspack.
Vite's default prefix may be changed in the config:ts
// vite.config.ts export const config = { // ...rest of your config envPrefix: 'MYPREFIX_', // this means `MYPREFIX_MY_VARIABLE` is the new correct way } - Restart development server after adding new variables
- Check file location: .env file must be in project root
- Verify bundler configuration: Ensure variables are properly injected
- Verify variable:
- In dev: is in correct .env file or environment
- For prod: is in correct .env file or current environment at bundle time. That's right, VITE_/PUBLIC_-prefixed variables are replaced in a macro-like fashion at bundle time, and will never be read at runtime on your server. This is a common mistake, so make sure this is not your case.
Example:
# ❌ Won't work (no prefix)
API_KEY=abc123
# ✅ Works with Vite
VITE_API_KEY=abc123
# ✅ Works with Rspack
PUBLIC_API_KEY=abc123
# ❌ Won't bundle the variable (assuming it is not set in the environment of the build)
npm run build
# ✅ Works with Vite and will bundle the variable for production
VITE_API_KEY=abc123 npm run build
# ✅ Works with Rspack and will bundle the variable for production
PUBLIC_API_KEY=abc123 npm run buildRuntime Client Environment Variables at Runtime in Production#
Problem: If VITE_/PUBLIC_ variables are replaced at bundle time only, how to make runtime variables available on the client ?
Solutions:
Pass variables from the server down to the client:
- Add your variable to the correct env. file
- Create an endpoint on your server to read the value from the client
Example:
You may use your prefered backend framework/libray, but here it is using Tanstack Start server functions:
const getRuntimeVar = createServerFn({ method: 'GET' }).handler(() => {
return process.env.MY_RUNTIME_VAR // notice `process.env` on the server, and no `VITE_`/`PUBLIC_` prefix
})
export const Route = createFileRoute('/')({
loader: async () => {
const foo = await getRuntimeVar()
return { foo }
},
component: RouteComponent,
})
function RouteComponent() {
const { foo } = Route.useLoaderData()
// ... use your variable however you want
}Variable Not Updating#
Problem: Environment variable changes aren't reflected in app
Solutions:
- Restart development server - Required for new variables
- Check file hierarchy - .env.local overrides .env
- Clear browser cache - Hard refresh (Ctrl+Shift+R)
- Verify correct file - Make sure you're editing the right .env file
TypeScript Errors#
Problem: Property 'VITE_MY_VAR' does not exist on type 'ImportMetaEnv'
Solution: Add declaration to src/vite-env.d.ts:
interface ImportMetaEnv {
readonly VITE_MY_VAR: string
}Build Errors#
Problem: Missing environment variables during build
Solutions:
- Configure CI/CD: Set variables in build environment
- Add validation: Check required variables at build time
- Use .env files: Ensure production .env files exist
- Check bundler config: Verify environment variable injection
Security Issues#
Problem: Accidentally exposing sensitive data
Solutions:
- Never use secrets in client variables - They're visible in browser
- Use server-side proxies for sensitive API calls
- Audit bundle - Check built files for leaked secrets
- Follow naming conventions - Only prefixed variables are exposed
Runtime vs Build-time Confusion#
Problem: Variables not available at runtime
Solutions:
- Understand static replacement - Variables are replaced at build time
- Use server-side for dynamic values - Use APIs for runtime configuration
- Validate at startup - Check all required variables exist
Environment Variables are Always Strings#
Problem: Unexpected behavior when comparing boolean or numeric values
Solutions:
- Always compare as strings: Use === 'true' not === true
- Convert explicitly: Use parseInt(), parseFloat(), or Boolean()
- Use helper functions: Create typed conversion utilities
Example:
// ❌ Won't work as expected
const isEnabled = import.meta.env.VITE_FEATURE_ENABLED // This is a string!
if (isEnabled) {
/* Always true if variable exists */
}
// ✅ Correct string comparison
const isEnabled = import.meta.env.VITE_FEATURE_ENABLED === 'true'
// ✅ Safe numeric conversion
const port = parseInt(import.meta.env.VITE_PORT || '3000', 10)
// ✅ Helper function approach
const getBooleanEnv = (value: string | undefined, defaultValue = false) => {
if (value === undefined) return defaultValue
return value.toLowerCase() === 'true'
}Common Next Steps#
Related Resources#
- TanStack Router File-Based Routing - Learn about route configuration
- Vite Environment Variables - Official Vite documentation
- Webpack DefinePlugin - Webpack environment configuration