Skip to content

alandotcom/standard-env

Repository files navigation

Standard Env

NPM Version

Type-safe, structured environment variable parsing using Standard Schema compatible validation libraries.

Features

  • 🔒 Type-safe - Full TypeScript support with automatic type inference
  • 🏗️ Structured config - Organize environment variables into nested objects
  • 🚀 Declarative API - Define your config structure in a single object
  • 🔄 Library agnostic - Works with any Standard Schema compatible library (arktype, zod, valibot, etc.)
  • 🎯 Default values & optional properties - Flexible configuration with type safety
  • 📦 Zero runtime dependencies - Lightweight and focused

Installation

bun add standardenv

# Also install your preferred validation library
bun add arktype  # or zod, valibot, etc.

Quick Start

import { envParse } from "standardenv";
import { type } from "arktype";

const config = envParse(process.env, {
  server: {
    port: {
      format: type('string.numeric.parse'),
      default: 3000,
      env: 'PORT'
    },
    nodeEnv: {
      format: type('"development" | "production" | "test"'),
      default: 'development',
      env: 'NODE_ENV'
    }
  },
  db: {
    url: {
      format: type('string'),
      env: 'DATABASE_URL'
    }
  }
});

// config.server.port is number (3000)
// config.server.nodeEnv is string ('development' | 'production' | 'test')
// config.db.url is string
// All fully typed with zero additional type definitions needed! ✨

Core Concepts

Declarative Configuration

Instead of separate schemas and defaults, define everything in one place:

const config = envParse(process.env, {
  // Nested structure for organization
  db: {
    url: {
      format: type('string'),           // Validation schema
      env: 'DATABASE_URL',             // Environment variable name
      // Required by default (no default provided)
    },
    maxConnections: {
      format: type('string.numeric.parse'),
      default: 10,                   // Default value
      env: 'DB_MAX_CONNECTIONS',
    }
  },

  server: {
    port: {
      format: type('string.numeric.parse'),
      default: 3000,
      env: 'PORT',
    },
    debug: {
      format: type('string').pipe(s => s === 'true'),
      default: false,
      env: 'DEBUG',
    }
  }
});

Property Configuration

Each property has these fields:

  • format: StandardSchema validator (required)
  • env: Environment variable name (required)
  • default: Default value (optional)
  • optional: If true, property can be undefined (optional)

Optional Properties

Mark properties as optional when they might not be set:

const config = envParse(process.env, {
  db: {
    url: {
      format: type('string'),
      env: 'DATABASE_URL', // Required - will throw if missing
    },
    redis: {
      url: {
        format: type('string'),
        env: 'REDIS_URL',
        optional: true, // Optional - will be undefined if not set
      }
    }
  },
  features: {
    analytics: {
      format: type('string').pipe(s => s === 'true'),
      env: 'ENABLE_ANALYTICS',
      optional: true, // Optional - will be undefined if not set
    }
  }
});

// TypeScript knows:
// config.db.url: string (required)
// config.db.redis.url: string | undefined (optional)
// config.features.analytics: boolean | undefined (optional)

Type Transformations

Environment variables are strings, but you often need other types:

Common Transformations with arktype

import { envParse } from "standardenv";
import { type } from "arktype";

const config = envParse(process.env, {
  server: {
    // String to number
    port: {
      format: type('string.numeric.parse'),
      default: 3000,
      env: 'PORT',
    },

    // String to boolean
    debug: {
      format: type('string').pipe(s => s === 'true' || s === '1'),
      default: false,
      env: 'DEBUG',
    },

    // String to array
    allowedOrigins: {
      format: type('string').pipe(s => s.split(',').map(origin => origin.trim())),
      default: ['http://localhost:3000'],
      env: 'ALLOWED_ORIGINS',
    },

    // String to JSON object
    featureFlags: {
      format: type('string').pipe((s): Record<string, boolean> => {
        try {
          return JSON.parse(s);
        } catch {
          return {};
        }
      }),
      default: {},
      env: 'FEATURE_FLAGS',
    }
  }
});

// Result types:
// config.server.port: number
// config.server.debug: boolean
// config.server.allowedOrigins: string[]
// config.server.featureFlags: Record<string, boolean>

Deeply Nested Configuration

Organize complex applications with deep nesting:

const config = envParse(process.env, {
  database: {
    primary: {
      url: {
        format: type('string'),
        env: 'DATABASE_URL',
      },
      maxConnections: {
        format: type('string.numeric.parse'),
        default: 20,
        env: 'DB_MAX_CONNECTIONS',
      }
    },
    cache: {
      redis: {
        url: {
          format: type('string'),
          env: 'REDIS_URL',
          optional: true,
        },
        ttl: {
          format: type('string.numeric.parse'),
          default: 3600,
          env: 'CACHE_TTL',
        }
      }
    }
  },

  auth: {
    jwt: {
      secret: {
        format: type('string'),
        env: 'JWT_SECRET',
      },
      expiresIn: {
        format: type('string'),
        default: '7d',
        env: 'JWT_EXPIRES_IN',
      }
    },
    oauth: {
      providers: {
        format: type('string').pipe(s => s.split(',')),
        default: ['google', 'github'],
        env: 'OAUTH_PROVIDERS',
      }
    }
  },

  logging: {
    level: {
      format: type('"debug" | "info" | "warn" | "error"'),
      default: 'info',
      env: 'LOG_LEVEL',
    },
    destination: {
      format: type('string'),
      default: 'console',
      env: 'LOG_DESTINATION',
      optional: true,
    }
  }
});

// Access with clean, organized structure:
// config.database.primary.url
// config.database.cache.redis.url
// config.auth.jwt.secret
// config.auth.oauth.providers
// config.logging.level

Library Compatibility

Works with any Standard Schema compatible library:

Arktype

import { type } from "arktype";

const config = envParse(process.env, {
  port: {
    format: type('string.numeric.parse'),
    env: 'PORT',
  }
});

Zod

import { z } from "zod";

const config = envParse(process.env, {
  port: {
    format: z.string().transform(Number),
    env: 'PORT',
  }
});

Valibot

import * as v from "valibot";

const config = envParse(process.env, {
  port: {
    format: v.pipe(v.string(), v.transform(Number)),
    env: 'PORT',
  }
});

Error Handling

Get clear error messages for validation failures:

import { EnvValidationError } from "standardenv";

try {
  const config = envParse(process.env, {
    port: {
      format: type('string.numeric.parse'),
      env: 'PORT', // Required, no default
    }
  });
} catch (error) {
  if (error instanceof EnvValidationError) {
    console.error("Environment validation failed:", error.message);
    console.error("Issues:", error.issues);
    console.error("Validator:", error.vendor);
  }
}

API Reference

envParse(env, config)

  • env: Record<string, string | undefined> - Environment variables (typically process.env)
  • config: ConfigDefinition - Declarative configuration structure

Returns the validated and typed configuration object with inferred types.

Configuration Properties

Each property in your config can have:

  • format (required): StandardSchema validator for the environment variable
  • env (required): Environment variable name to read from
  • default (optional): Default value (must be string - will be validated by format)
  • optional (optional): If true, property will be T | undefined instead of T

Error Classes

  • EnvValidationError - Thrown when environment variables fail validation
  • AsyncValidationError - Thrown when async validation is attempted (not supported)

Best Practices

✅ Do

// Organize related config into nested objects
const config = envParse(process.env, {
  database: {
    url: { format: type('string'), env: 'DATABASE_URL' },
    poolSize: { format: type('string.numeric.parse'), default: 10, env: 'DB_POOL_SIZE' }
  }
});

// Use meaningful default values
const config = envParse(process.env, {
  server: {
    port: { format: type('string.numeric.parse'), default: 3000, env: 'PORT' }
  }
});

// Mark truly optional config as optional
const config = envParse(process.env, {
  monitoring: {
    sentryDsn: { format: type('string'), env: 'SENTRY_DSN', optional: true }
  }
});

❌ Don't

// Don't use non-string types in format without transformation
const config = envParse(process.env, {
  port: { format: type('number'), env: 'PORT' } // ❌ Will fail - env vars are strings
});

// Don't put defaults that don't match the expected format
const config = envParse(process.env, {
  port: { format: type('string.numeric.parse'), default: '3000', env: 'PORT' } // ❌ Default should be number
});

Complete Example

import { envParse } from "standardenv";
import { type } from "arktype";

export const config = envParse(process.env, {
  app: {
    name: {
      format: type('string'),
      default: 'my-app',
      env: 'APP_NAME',
    },
    version: {
      format: type('string'),
      default: '1.0.0',
      env: 'APP_VERSION',
    }
  },

  server: {
    port: {
      format: type('string.numeric.parse'),
      default: 3000,
      env: 'PORT',
    },
    host: {
      format: type('string'),
      default: '0.0.0.0',
      env: 'HOST',
    },
    cors: {
      origins: {
        format: type('string').pipe(s => s.split(',').map(o => o.trim())),
        default: ['http://localhost:3000'],
        env: 'CORS_ORIGINS',
      }
    }
  },

  database: {
    url: {
      format: type('string'),
      env: 'DATABASE_URL',
    },
    ssl: {
      format: type('string').pipe(s => s === 'true'),
      default: false,
      env: 'DATABASE_SSL',
    }
  },

  auth: {
    clerk: {
      secretKey: {
        format: type('string'),
        env: 'CLERK_SECRET_KEY',
      },
      publishableKey: {
        format: type('string'),
        env: 'CLERK_PUBLISHABLE_KEY',
      }
    }
  },

  features: {
    analytics: {
      format: type('string').pipe(s => s === 'true'),
      default: false,
      env: 'ENABLE_ANALYTICS',
    },
    monitoring: {
      format: type('string').pipe(s => s === 'true'),
      default: false,
      env: 'ENABLE_MONITORING',
      optional: true,
    }
  }
});

// config is fully typed as:
// {
//   app: { name: string; version: string };
//   server: { port: number; host: string; cors: { origins: string[] } };
//   database: { url: string; ssl: boolean };
//   auth: { clerk: { secretKey: string; publishableKey: string } };
//   features: { analytics: boolean; monitoring?: boolean };
// }

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published