Type-safe, structured environment variable parsing using Standard Schema compatible validation libraries.
- 🔒 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
bun add standardenv
# Also install your preferred validation library
bun add arktype # or zod, valibot, etc.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! ✨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',
}
}
});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)
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)Environment variables are strings, but you often need other types:
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>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.levelWorks with any Standard Schema compatible library:
import { type } from "arktype";
const config = envParse(process.env, {
port: {
format: type('string.numeric.parse'),
env: 'PORT',
}
});import { z } from "zod";
const config = envParse(process.env, {
port: {
format: z.string().transform(Number),
env: 'PORT',
}
});import * as v from "valibot";
const config = envParse(process.env, {
port: {
format: v.pipe(v.string(), v.transform(Number)),
env: 'PORT',
}
});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);
}
}- env:
Record<string, string | undefined>- Environment variables (typicallyprocess.env) - config:
ConfigDefinition- Declarative configuration structure
Returns the validated and typed configuration object with inferred types.
Each property in your config can have:
format(required): StandardSchema validator for the environment variableenv(required): Environment variable name to read fromdefault(optional): Default value (must be string - will be validated by format)optional(optional): If true, property will beT | undefinedinstead ofT
EnvValidationError- Thrown when environment variables fail validationAsyncValidationError- Thrown when async validation is attempted (not supported)
// 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 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
});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 };
// }