Skip to main content

Overview

Load Env provides type-safe functions for accessing environment variables with automatic validation and optional fallback values. Each function ensures the value matches the expected type and throws descriptive errors when validation fails.

Available Functions

All functions follow the pattern env<Type>(key, fallback?) and maybeEnv<Type>(key):
  • env* functions require a value (throws if missing)
  • maybeEnv* functions return undefined if missing
  • Both support optional fallback values

String Values

envString

Read a required string environment variable:
import { envString } from "@axel/load-env"

// Throws if NODE_ENV is missing or empty
export const NODE_ENV = envString("NODE_ENV")

// With fallback
export const NODE_ENV = envString("NODE_ENV", "development")

maybeEnvString

Read an optional string environment variable:
import { maybeEnvString } from "@axel/load-env"

// Returns string | undefined
export const CUSTOM_HEADER = maybeEnvString("CUSTOM_HEADER")

if (CUSTOM_HEADER) {
  console.log(`Using custom header: ${CUSTOM_HEADER}`)
}

Boolean Values

envBool

Read a required boolean environment variable:
import { envBool } from "@axel/load-env"

// Accepts: true, false, 1, 0, yes, no, on, off (case insensitive)
export const CI = envBool("CI", false)
export const DEBUG = envBool("DEBUG")
Accepted boolean values: true, false, 1, 0, yes, no, on, off (case-insensitive)

maybeEnvBool

Read an optional boolean environment variable:
import { maybeEnvBool } from "@axel/load-env"

export const ENABLE_FEATURE = maybeEnvBool("ENABLE_FEATURE")

if (ENABLE_FEATURE) {
  console.log("Feature enabled")
}

Numeric Values

envInt

Read a required integer environment variable:
import { envInt } from "@axel/load-env"

export const PORT = envInt("PORT", 3000)
export const MAX_CONNECTIONS = envInt("MAX_CONNECTIONS")

envFloat

Read a required floating-point number:
import { envFloat } from "@axel/load-env"

export const ZOOM = envFloat("ZOOM", 1.0)
export const RATE_LIMIT = envFloat("RATE_LIMIT")

maybeEnvInt and maybeEnvFloat

Read optional numeric environment variables:
import { maybeEnvInt, maybeEnvFloat } from "@axel/load-env"

export const TIMEOUT = maybeEnvInt("TIMEOUT") ?? 5000
export const SCALE = maybeEnvFloat("SCALE") ?? 1.0

Enumerated Values

envEnum

Constrain values to a specific set of strings:
import { envEnum } from "@axel/load-env"

export const NODE_ENV = envEnum(
  "NODE_ENV",
  ["development", "production", "test"] as const,
  "development",
)

export const LOG_LEVEL = envEnum(
  "LOG_LEVEL",
  ["debug", "info", "warn", "error"] as const,
)
The as const assertion ensures TypeScript infers the exact string literal types, not just string.

maybeEnvEnum

Read an optional enumerated value:
import { maybeEnvEnum } from "@axel/load-env"

export const LOG_LEVEL = maybeEnvEnum(
  "LOG_LEVEL",
  ["debug", "info", "warn", "error"] as const,
)

// Type: "debug" | "info" | "warn" | "error" | undefined

URL Values

envUrl

Read a required URL environment variable:
import { envUrl } from "@axel/load-env"

export const DATABASE_URL = envUrl("DATABASE_URL")
export const API_URL = envUrl("API_URL", new URL("http://localhost"))

// Use as a URL object
console.log(DATABASE_URL.hostname)
console.log(DATABASE_URL.pathname)

maybeEnvUrl

Read an optional URL environment variable:
import { maybeEnvUrl } from "@axel/load-env"

export const WEBHOOK_URL = maybeEnvUrl("WEBHOOK_URL")

if (WEBHOOK_URL) {
  await fetch(WEBHOOK_URL, { method: "POST", body: data })
}

Date Values

envDate

Read a required date environment variable:
import { envDate } from "@axel/load-env"

// Accepts any format parseable by new Date()
export const START_DATE = envDate("START_DATE")
export const DEPLOYMENT_DATE = envDate("DEPLOYMENT_DATE", new Date())

maybeEnvDate

Read an optional date environment variable:
import { maybeEnvDate } from "@axel/load-env"

export const END_DATE = maybeEnvDate("END_DATE")

if (END_DATE && new Date() > END_DATE) {
  console.log("Campaign has ended")
}

UUID Values

envUuid

Read a required UUID environment variable:
import { envUuid } from "@axel/load-env"

// Validates UUID format
export const API_TOKEN = envUuid("API_TOKEN")
export const TENANT_ID = envUuid("TENANT_ID")

maybeEnvUuid

Read an optional UUID environment variable:
import { maybeEnvUuid } from "@axel/load-env"

export const REQUEST_ID = maybeEnvUuid("REQUEST_ID")

Array Values

envStrings

Read a comma-separated list of strings:
import { envStrings } from "@axel/load-env"

// ALLOWED_ORIGINS=https://example.com,https://app.example.com
export const ALLOWED_ORIGINS = envStrings("ALLOWED_ORIGINS", ["*"])

// Values are automatically trimmed and empty strings filtered
// "a, b , c" -> ["a", "b", "c"]

maybeEnvStrings

Read an optional comma-separated list:
import { maybeEnvStrings } from "@axel/load-env"

export const FEATURE_FLAGS = maybeEnvStrings("FEATURE_FLAGS")

// Returns string[] | undefined
if (FEATURE_FLAGS?.includes("new-ui")) {
  console.log("New UI enabled")
}

Error Handling

All functions throw descriptive errors when validation fails:

Missing Values

import { envString } from "@axel/load-env"

try {
  const API_KEY = envString("API_KEY")
} catch (error) {
  // Error: $API_KEY is missing
  console.error(error.message)
}

Type Validation Errors

import { envInt } from "@axel/load-env"

// PORT=abc
try {
  const PORT = envInt("PORT")
} catch (error) {
  // TypeError: $PORT is not a number: abc
  console.error(error.message)
}

Enum Validation Errors

import { envEnum } from "@axel/load-env"

// LOG_LEVEL=trace
try {
  const LOG_LEVEL = envEnum(
    "LOG_LEVEL",
    ["debug", "info", "warn", "error"] as const,
  )
} catch (error) {
  // TypeError: $LOG_LEVEL is not one of debug, info, warn, error: trace
  console.error(error.message)
}

Implementation Details

Each function:
  1. Reads the value from process.env[key]
  2. Trims whitespace from the value
  3. Falls back to the fallback value if provided
  4. Throws if the value is undefined and no fallback
  5. Validates and converts the value to the target type
  6. Throws a TypeError if validation fails
Example from the source:
export function envInt(key: string, fallback?: number): number {
  const str = process.env[key]?.trim() || fallback?.toString().trim()
  if (str === undefined) throw new Error(`$${key} is missing`)

  const num = parseInt(str)
  if (isNaN(num)) throw new TypeError(`$${key} is not a number: ${str}`)
  return num
}

Best Practices

Export Configuration Values

Create a dedicated configuration module:
// config.ts
import { envString, envInt, envBool, envUrl } from "@axel/load-env"

export const NODE_ENV = envString("NODE_ENV", "development")
export const PORT = envInt("PORT", 3000)
export const DATABASE_URL = envUrl("DATABASE_URL")
export const ENABLE_LOGGING = envBool("ENABLE_LOGGING", true)
Then import from this module throughout your application:
import { PORT, DATABASE_URL } from "./config.js"

const server = createServer()
server.listen(PORT)

Use Fallback Values Wisely

Use fallbacks for development convenience:
export const PORT = envInt("PORT", 3000)
export const LOG_LEVEL = envEnum(
  "LOG_LEVEL",
  ["debug", "info", "warn", "error"] as const,
  "debug",
)

Type Safety with TypeScript

TypeScript automatically infers the correct types:
import { envInt, envString, maybeEnvBool } from "@axel/load-env"

const PORT = envInt("PORT") // type: number
const NODE_ENV = envString("NODE_ENV") // type: string
const DEBUG = maybeEnvBool("DEBUG") // type: boolean | undefined

Next Steps

Docker Secrets

Learn how to load secrets from files in Docker environments

Type Safety

Understand how TypeScript types are enforced throughout the library

Build docs developers (and LLMs) love