Skip to content

Bun Environment Variables

Bun provides powerful environment variable management, automatically loading .env files, and offering multiple access methods. This chapter introduces Bun environment variables usage.

Auto-loading .env Files

Basic Usage

Bun automatically loads .env files in the project root:

bash
# .env
APP_NAME=myapp
APP_PORT=3000
DATABASE_URL=postgresql://localhost:5432/mydb
API_KEY=your-secret-key-here
DEBUG=true
typescript
// app.ts
console.log(Bun.env.APP_NAME);     // "myapp"
console.log(Bun.env.APP_PORT);     // "3000"
console.log(Bun.env.DATABASE_URL); // "postgresql://localhost:5432/mydb"

Run:

bash
bun app.ts

Loading Order

Bun loads environment variables in the following order (later values override earlier ones):

  1. System environment variables
  2. .env file
  3. .env.local file
  4. .env.development / .env.production (based on NODE_ENV)
  5. .env.development.local / .env.production.local

Accessing Environment Variables

Using Bun.env

typescript
// Bun recommended way
const appName = Bun.env.APP_NAME;
const port = Bun.env.APP_PORT;

// With defaults
const debug = Bun.env.DEBUG ?? "false";
const timeout = Bun.env.TIMEOUT || "5000";

Using process.env

typescript
// Node.js compatible way
const appName = process.env.APP_NAME;
const port = process.env.APP_PORT;

// Equivalent to Bun.env
console.log(Bun.env === process.env); // true

Type-safe Access

typescript
// Define environment variable interface
interface Env {
  APP_NAME: string;
  APP_PORT: string;
  DATABASE_URL: string;
  API_KEY: string;
  DEBUG?: string;
}

// Type assertion
const env = Bun.env as unknown as Env;

// Or create config object
const config = {
  appName: Bun.env.APP_NAME!,
  port: parseInt(Bun.env.APP_PORT || "3000"),
  databaseUrl: Bun.env.DATABASE_URL!,
  apiKey: Bun.env.API_KEY!,
  debug: Bun.env.DEBUG === "true",
};

Multi-environment Configuration

Environment Files

bash
# .env - Shared across all environments
APP_NAME=myapp

# .env.development - Development environment
NODE_ENV=development
API_URL=http://localhost:3000
DEBUG=true

# .env.production - Production environment
NODE_ENV=production
API_URL=https://api.example.com
DEBUG=false

# .env.local - Local overrides (not committed to Git)
API_KEY=my-local-key

.gitignore Configuration

gitignore
# Don't commit sensitive local config
.env.local
.env.*.local
.env.development.local
.env.production.local

Specifying Environment Files

bash
# Use --env-file to specify file
bun --env-file .env.staging app.ts

# Can be used multiple times
bun --env-file .env --env-file .env.staging app.ts

Environment Variable Syntax

Basic Syntax

bash
# Simple assignment
KEY=value

# With quotes (preserve spaces)
MESSAGE="Hello World"
SINGLE='Hello World'

# Multi-line value
MULTILINE="Line 1
Line 2
Line 3"

# Empty value
EMPTY=
EMPTY_QUOTED=""

Variable References

bash
# Reference other variables
BASE_URL=https://api.example.com
API_ENDPOINT=${BASE_URL}/v1

# With defaults
PORT=${APP_PORT:-3000}
HOST=${APP_HOST:-localhost}

Comments

bash
# This is a comment
APP_NAME=myapp  # End-of-line comment

# Multi-line comment
# API_KEY=old-key
API_KEY=new-key

Escape Characters

bash
# Include special characters
PASSWORD="pass\$word"
PATH_WITH_SPACES="/path/to/my\ folder"

Configuration Validation

Startup Validation

typescript
// config.ts
function getRequiredEnv(key: string): string {
  const value = Bun.env[key];
  if (!value) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
  return value;
}

function getOptionalEnv(key: string, defaultValue: string): string {
  return Bun.env[key] || defaultValue;
}

export const config = {
  // Required environment variables
  databaseUrl: getRequiredEnv("DATABASE_URL"),
  apiKey: getRequiredEnv("API_KEY"),
  
  // Optional environment variables
  port: parseInt(getOptionalEnv("PORT", "3000")),
  debug: getOptionalEnv("DEBUG", "false") === "true",
  logLevel: getOptionalEnv("LOG_LEVEL", "info"),
};

console.log("Config loaded successfully:", config);

Using Zod Validation

typescript
// config.ts
import { z } from "zod";

const envSchema = z.object({
  NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
  APP_PORT: z.string().transform(Number).default("3000"),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(10),
  DEBUG: z.string().transform(v => v === "true").default("false"),
});

export const config = envSchema.parse(Bun.env);

// Type inference
// config.NODE_ENV: "development" | "production" | "test"
// config.APP_PORT: number
// config.DEBUG: boolean

Dynamically Setting Environment Variables

Setting in Code

typescript
// Set environment variables
Bun.env.NEW_VAR = "new value";
process.env.ANOTHER_VAR = "another value";

// Read
console.log(Bun.env.NEW_VAR);      // "new value"
console.log(Bun.env.ANOTHER_VAR);  // "another value"

// Delete
delete Bun.env.NEW_VAR;
console.log(Bun.env.NEW_VAR);      // undefined

Subprocess Environment Variables

typescript
import { $ } from "bun";

// Pass environment variables to subprocess
const result = await $`echo $MY_VAR`.env({
  MY_VAR: "Hello from parent"
}).text();

console.log(result);  // "Hello from parent"

Security Best Practices

Sensitive Information Handling

typescript
// ❌ Don't do this
console.log("API Key:", Bun.env.API_KEY);

// ✅ Safe logging
console.log("API Key:", Bun.env.API_KEY ? "***SET***" : "NOT SET");

// ✅ Use masking
function maskSensitive(value: string): string {
  if (!value || value.length < 8) return "***";
  return value.slice(0, 4) + "***" + value.slice(-4);
}

console.log("API Key:", maskSensitive(Bun.env.API_KEY || ""));

Environment Variable Template

Create .env.example as a template:

bash
# .env.example - Commit to version control
# Copy this file to .env and fill in actual values

# App configuration
APP_NAME=myapp
APP_PORT=3000

# Database configuration (required)
DATABASE_URL=postgresql://user:password@localhost:5432/dbname

# API key (required)
API_KEY=your-api-key-here

# Optional configuration
DEBUG=false
LOG_LEVEL=info

Secret Management

typescript
// For production, use secret management service
import { SecretsManager } from "@aws-sdk/client-secrets-manager";

async function loadSecrets() {
  if (Bun.env.NODE_ENV === "production") {
    const client = new SecretsManager({ region: "ap-northeast-1" });
    const response = await client.getSecretValue({ SecretId: "my-app/prod" });
    const secrets = JSON.parse(response.SecretString!);
    
    // Merge into environment variables
    Object.assign(Bun.env, secrets);
  }
}

await loadSecrets();

bunfig.toml Environment Configuration

toml
# bunfig.toml

[run]
# Specify environment files
env-file = [".env", ".env.local"]

# Set environment variables
[run.env]
NODE_ENV = "development"
LOG_LEVEL = "debug"

Special Environment Variables

Bun-specific Variables

VariableDescription
BUN_INSTALLBun installation directory
BUN_CONFIG_VERBOSE_FETCHPrint fetch request details
BUN_CONFIG_MAX_HTTP_CONNECTIONSMax HTTP connections
BUN_JSC_*JavaScriptCore engine options

Node.js Compatible Variables

VariableDescription
NODE_ENVRuntime environment
NODE_PATHModule search path
NODE_DEBUGDebug module

Practical Examples

Complete Configuration Management

typescript
// src/config/index.ts
import { z } from "zod";

// Environment variable schema
const envSchema = z.object({
  // App
  NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
  APP_NAME: z.string().default("my-app"),
  APP_PORT: z.coerce.number().default(3000),
  
  // Database
  DATABASE_URL: z.string().url(),
  DATABASE_POOL_SIZE: z.coerce.number().default(10),
  
  // Redis
  REDIS_URL: z.string().url().optional(),
  
  // Authentication
  JWT_SECRET: z.string().min(32),
  JWT_EXPIRES_IN: z.string().default("7d"),
  
  // External services
  API_KEY: z.string().optional(),
  
  // Logging
  LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info"),
  
  // Feature flags
  FEATURE_NEW_UI: z.coerce.boolean().default(false),
});

// Parse and export
const parsed = envSchema.safeParse(Bun.env);

if (!parsed.success) {
  console.error("❌ Environment variable configuration error:");
  console.error(parsed.error.format());
  process.exit(1);
}

export const config = parsed.data;

// Derived config
export const isDev = config.NODE_ENV === "development";
export const isProd = config.NODE_ENV === "production";
export const isTest = config.NODE_ENV === "test";

Using Configuration

typescript
// src/index.ts
import { config, isDev } from "./config";

const server = Bun.serve({
  port: config.APP_PORT,
  
  fetch(request) {
    if (isDev) {
      console.log("Received request:", request.url);
    }
    
    return new Response(`Welcome to ${config.APP_NAME}!`);
  },
});

console.log(`${config.APP_NAME} running on port ${server.port}`);

Summary

This chapter covered:

  • ✅ Auto-loading .env files
  • ✅ Multiple ways to access environment variables
  • ✅ Multi-environment configuration management
  • ✅ Environment variable syntax and variable references
  • ✅ Configuration validation and type safety
  • ✅ Security best practices

Next Steps

Continue reading File I/O to learn about Bun's high-performance file handling capabilities.

Content is for learning and research only.