UNPKG

@maxifjaved/elysia-env

Version:

Type-safe environment variable validation for Elysia.js - feels native, works beautifully

196 lines (183 loc) 5.25 kB
import type { TObject, TProperties, Static } from "@sinclair/typebox"; import { Value } from "@sinclair/typebox/value"; import { Elysia, t } from "elysia"; /** * Options for environment variable validation. * * @template T - TypeBox schema properties type */ export type EnvOptions<T extends TProperties> = { /** * Custom environment variables source to use instead of process.env. * Useful for testing, secret managers, or custom configuration sources. * * @example * ```ts * // Using custom source for testing * createEnv(schema, { * source: { API_KEY: 'test-key', PORT: '3000' } * }) * ``` * * @example * ```ts * // Combining multiple sources * createEnv(schema, { * source: { ...process.env, ...await getSecrets() } * }) * ``` * * @default process.env */ source?: Record<string, unknown>; /** * Only load environment variables with this prefix. * The prefix will be stripped from variable names in the validated output. * * Useful for monorepos or when multiple apps share the same environment. * * @example * ```ts * // With process.env = { APP_API_KEY: 'abc', APP_PORT: '3000', OTHER: 'xyz' } * createEnv( * t.Object({ API_KEY: t.String(), PORT: t.Numeric() }), * { prefix: 'APP_' } * ) * // Results in: { API_KEY: 'abc', PORT: 3000 } * ``` */ prefix?: string; /** * Defines how to handle validation errors. * * - `'exit'`: Log error and exit process with code 1 (default, recommended for production) * - `'warn'`: Log warning and continue execution (useful for development) * - `'silent'`: Continue without logging (use with custom onError handler) * - `function`: Custom error handler receiving error details * * @example * ```ts * createEnv(schema, { * onError: (errors) => { * logToSentry(errors); * throw new Error('Invalid environment configuration'); * } * }) * ``` * * @default 'exit' */ onError?: | "exit" | "warn" | "silent" | ((errors: Record<string, string>) => void); /** * Callback executed after successful validation. * Useful for logging, analytics, or additional processing. * * @example * ```ts * createEnv(schema, { * onSuccess: (env) => { * console.log(`✅ Loaded config for ${env.APP_NAME}`); * } * }) * ``` */ onSuccess?: (env: Static<TObject<T>>) => void; }; /** * Validates environment variables - feels native to Elysia. * * @template T - TypeBox schema properties * @param variables - Schema properties (not wrapped in t.Object) * @param options - Optional configuration * @returns Elysia plugin with validated env * * @example * Clean and simple - just like Elysia: * ```ts * import { Elysia, t } from 'elysia' * import { env } from '@maxifjaved/elysia-env' * * new Elysia() * .use(env({ * PORT: t.Numeric({ default: 3000 }), * API_KEY: t.String({ minLength: 10 }) * })) * .get('/', ({ env }) => env.PORT) // Fully typed! * .listen(3000) * ``` */ export function env<T extends TProperties>( variables: T, options: EnvOptions<T> = {}, ) { return new Elysia({ name: "env" }).decorate(() => { const schema = t.Object(variables); const { source = process.env, prefix, onError = "exit", onSuccess, } = options; // Apply prefix filtering if specified let processedSource = source; if (prefix) { processedSource = Object.entries(source).reduce( (acc, [key, value]) => { if (key.startsWith(prefix)) { const newKey = key.substring(prefix.length); acc[newKey] = value; } return acc; }, {} as Record<string, unknown>, ); } // Apply transformations using Value.Parse (like @yolk-oss/elysia-env) // Order: Clean → Default → Decode → Convert const processed = Value.Parse( ['Clean', 'Default', 'Decode', 'Convert'], schema, processedSource, ); // Validate against schema if (!Value.Check(schema, processed)) { const errors = [...Value.Errors(schema, processed)].reduce( (acc, e) => { const path = e.path.substring(1) || "root"; acc[path] = e.message; return acc; }, {} as Record<string, string>, ); if (typeof onError === "function") { onError(errors); } else { const errorMessage = Object.entries(errors) .map(([key, msg]) => ` - ${key}: ${msg}`) .join("\n"); switch (onError) { case "exit": console.error(`❌ Invalid environment variables:\n${errorMessage}`); process.exit(1); case "warn": console.warn(`⚠️ Invalid environment variables:\n${errorMessage}`); break; case "silent": break; } } } else { // Call success callback if validation passed onSuccess?.(processed as Static<TObject<T>>); } return { env: processed as Static<typeof schema>, }; }); } // Keep createEnv as alias for those who prefer explicit naming export const createEnv = env;