schema-env
Version:
Type-safe environment variable validation for Node.js using Zod schemas or custom adapters. Load .env files, expand variables, fetch async secrets, and validate process.env at startup.
195 lines (192 loc) • 10.9 kB
TypeScript
import dotenv from 'dotenv';
import { z } from 'zod';
type DotenvExpandFunction = (config: dotenv.DotenvConfigOutput) => dotenv.DotenvConfigOutput;
/** Input for schema validation, potentially holding values from all sources. */
type EnvironmentInput = Record<string, unknown>;
/** Function signature for fetching secrets asynchronously. */
type SecretSourceFunction = () => Promise<Record<string, string | undefined>>;
/** Standardized error format for validation failures. */
interface StandardizedValidationError {
/** Path to the invalid field. */
path: (string | number)[];
/** Description of the validation failure. */
message: string;
}
/** Standardized result structure for validation adapters. */
type ValidationResult<TResult> = {
success: true;
data: TResult;
} | {
success: false;
error: {
issues: StandardizedValidationError[];
};
};
/**
* Interface for validation library adapters.
* Allows plugging in different validation libraries (Zod, Joi, Yup, etc.).
* @template TResult The expected shape of the validated environment object.
*/
interface ValidatorAdapter<TResult> {
/**
* Validates the merged environment data.
* @param data The raw, merged environment data object.
* @returns A `ValidationResult` indicating success or failure.
*/
validate(data: EnvironmentInput): ValidationResult<TResult>;
}
/**
* Base options common to both `createEnv` and `createEnvAsync`.
* You must provide EITHER `schema` (for default Zod validation) OR `validator`.
*
* @template TSchema The Zod object schema type if using the default Zod validator.
* @template TResult The expected type of the validated environment object.
*/
interface CreateEnvBaseOptions<TSchema extends z.ZodSchema | undefined, TResult> {
/**
* The Zod schema defining expected environment variables.
* Required if a custom `validator` is NOT provided.
* If provided, it MUST be a `z.object({...})`.
* Mutually exclusive with `validator`.
*/
schema?: TSchema;
/**
* A custom validation adapter instance conforming to the `ValidatorAdapter` interface.
* Required if `schema` is NOT provided.
* If provided, the return type `TResult` must typically be specified via a
* generic type argument on `createEnv`/`createEnvAsync` (e.g., `createEnv<undefined, MyType>({...})`).
* Mutually exclusive with `schema`.
*/
validator?: ValidatorAdapter<TResult>;
/**
* Optional: Path or array of paths to .env files to load.
* - Defaults to './.env' relative to `process.cwd()`.
* - If an array is provided (e.g., `['.env.base', '.env.local']`), files are loaded sequentially,
* with variables in later files overriding earlier ones. Non-string entries are ignored with a warning.
* - Set to `false` to disable loading all .env files (including the environment-specific one).
*
* Note on Environment-Specific File: Regardless of whether a single path or an array
* is provided (unless `dotEnvPath` is `false`), if `process.env.NODE_ENV` is set,
* an attempt will be made to load an environment-specific file (e.g., `.env.development`)
* *after* all files specified in `dotEnvPath` have been loaded. This environment-specific
* file will override variables from the files specified in `dotEnvPath`.
*/
dotEnvPath?: string | false | string[];
/**
* Optional: Enable variable expansion using `dotenv-expand`. Defaults to `false`.
* Expansion is performed on the combined values from all loaded `.env` files
* (including the environment-specific file if loaded) *before* merging with
* `process.env` (for `createEnv`) or secrets (for `createEnvAsync`) and validation.
* Variables in `process.env` or secrets are NOT expanded.
*/
expandVariables?: boolean;
/** @internal */
_internalDotenvExpand?: DotenvExpandFunction;
}
/**
* Options for the synchronous `createEnv` function.
* Requires either `schema` (a Zod object schema) OR `validator`.
*
* @template TSchema The Zod object schema type if using the default Zod validator.
* @template TResult The expected type of the validated environment object. Inferred if TSchema is provided.
*/
type CreateEnvOptions<TSchema extends z.AnyZodObject | undefined, TResult = TSchema extends z.AnyZodObject ? z.infer<TSchema> : unknown> = CreateEnvBaseOptions<TSchema, TResult>;
/**
* Options for the asynchronous `createEnvAsync` function.
* Requires either `schema` (a Zod object schema) OR `validator`.
*
* @template TSchema The Zod object schema type if using the default Zod validator.
* @template TResult The expected type of the validated environment object. Inferred if TSchema is provided.
*/
interface CreateEnvAsyncOptions<TSchema extends z.AnyZodObject | undefined, TResult = TSchema extends z.AnyZodObject ? z.infer<TSchema> : unknown> extends CreateEnvBaseOptions<TSchema, TResult> {
/**
* Optional: An array of functions that fetch secrets asynchronously.
* Each function should return a Promise resolving to a `Record<string, string | undefined>`.
* Secrets fetched here will override variables from `.env` files but be overridden by `process.env`.
* Fetching errors are logged as warnings, but do not halt execution unless all sources fail.
*
* @example
* ```js
* const getSecretsFromAWS = async () => {
* // Your logic to fetch from AWS Secrets Manager
* return { DB_PASSWORD: 'fetchedPassword' };
* }
* const getSecretsFromVault = async () => {
* // Your logic to fetch from HashiCorp Vault
* return { API_KEY: 'fetchedApiKey' };
* }
*
* createEnvAsync({
* schema, // Or validator
* secretsSources: [getSecretsFromAWS, getSecretsFromVault]
* })
* ```
*/
secretsSources?: SecretSourceFunction[];
}
/**
* Validates and parses environment variables synchronously.
* Supports Zod schema validation (default via `schema` option) or a custom
* validation library via the `validator` option.
*
* Loads variables from `.env` files (specified paths, base, and environment-specific) and `process.env`.
* Optionally expands variables using `dotenv-expand`.
* Throws an error if validation fails, ensuring environment safety at startup.
*
* Use this for standard synchronous initialization. For fetching secrets from
* external systems asynchronously, use `createEnvAsync`.
*
* You **must** provide either the `schema` option (for default Zod validation)
* or the `validator` option (for custom validation), but not both.
*
* The final precedence order for variables is:
* 1. `process.env` (Highest priority)
* 2. Environment-specific file (e.g., `.env.production`) if `NODE_ENV` is set and `dotEnvPath` is not false.
* 3. Files specified in `dotEnvPath` array (later files override earlier ones) / Single `dotEnvPath` file / Default `./.env` (if `dotEnvPath` is not false).
* 4. Defaults defined in the validation schema/logic (Lowest priority - applied by Zod or custom adapter during validation).
*
* Note: Variable expansion (`expandVariables: true`) happens *after* all `.env` files (2, 3) are merged,
* but *before* merging with `process.env` (1).
*
* @template TSchema - The Zod object schema type (`z.AnyZodObject`) if using default validation. Leave `undefined` if using `validator`.
* @template TResult - The expected type of the validated environment object. Inferred from TSchema if using Zod, otherwise requires explicit specification (e.g., `createEnv<undefined, MyCustomType>({ validator: ... })`).
* @param options - Configuration options. Requires either `schema` OR `validator`.
* @returns {TResult} The validated environment object.
* @throws {Error} If validation fails, options are invalid (e.g., both `schema` and `validator` provided, or neither), or file loading encounters critical errors.
*/
declare function createEnv<TSchema extends z.AnyZodObject | undefined, TResult = TSchema extends z.AnyZodObject ? z.infer<TSchema> : unknown>(options: CreateEnvOptions<TSchema, TResult>): TResult;
/**
* Validates and parses environment variables asynchronously.
* Supports Zod schema validation (default via `schema` option) or a custom
* validation library via the `validator` option.
*
* Loads variables from `.env` files, optional asynchronous `secretsSources`, and `process.env`.
* Optionally expands variables from `.env` files using `dotenv-expand`.
* Returns a Promise that resolves with the validated environment or rejects if validation fails.
*
* Use this when you need to fetch secrets from external systems during startup.
* For purely synchronous validation, use `createEnv`.
*
* You **must** provide either the `schema` option (for default Zod validation)
* or the `validator` option (for custom validation), but not both.
*
* The final precedence order for variables is:
* 1. `process.env` (Highest priority)
* 2. Variables fetched via `secretsSources` (Later sources override earlier ones).
* 3. Environment-specific file (e.g., `.env.production`) if `NODE_ENV` is set and `dotEnvPath` is not false.
* 4. Files specified in `dotEnvPath` array (later files override earlier ones) / Single `dotEnvPath` file / Default `./.env` (if `dotEnvPath` is not false).
* 5. Defaults defined in the validation schema/logic (Lowest priority - applied by Zod or custom adapter during validation).
*
* Note: Variable expansion (`expandVariables: true`) happens *after* all `.env` files (3, 4) are merged,
* but *before* merging with `secretsSources` (2) and `process.env` (1).
*
* @template TSchema - The Zod object schema type (`z.AnyZodObject`) if using default validation. Leave `undefined` if using `validator`.
* @template TResult - The expected type of the validated environment object. Inferred from TSchema if using Zod, otherwise requires explicit specification (e.g., `createEnvAsync<undefined, MyCustomType>({ validator: ... })`).
* @param options - Configuration options. Requires either `schema` OR `validator`.
* @returns {Promise<TResult>} A Promise resolving to the validated environment object.
* @throws {Error} If options are invalid (e.g., both `schema` and `validator` provided, or neither) (synchronous throw).
* @throws {Error} If synchronous file loading encounters critical errors (synchronous throw).
* @rejects {Error} If asynchronous operations or validation fail.
*/
declare function createEnvAsync<TSchema extends z.AnyZodObject | undefined, TResult = TSchema extends z.AnyZodObject ? z.infer<TSchema> : unknown>(options: CreateEnvAsyncOptions<TSchema, TResult>): Promise<TResult>;
export { type CreateEnvAsyncOptions, type CreateEnvOptions, type SecretSourceFunction, type StandardizedValidationError, type ValidationResult, type ValidatorAdapter, createEnv, createEnvAsync };