@periodic/zirconium
v1.0.0
Published
Production-grade, enterprise-safe environment configuration engine for Node.js with strict coercion, environment scoping, and schema introspection
Maintainers
Readme
🔧 Periodic Zirconium
Production-grade, enterprise-safe environment configuration engine for Node.js with TypeScript support
Part of the Periodic series of Node.js packages by Uday Thakur.
💡 Why Zirconium?
Zirconium gets its name from the chemical element renowned for its exceptional resistance — to heat, corrosion, and chemical attack. Just like zirconium holds its structure under extreme conditions, this library holds your configuration together under pressure — turning missing or malformed environment variables into clear, actionable errors before they take down your app.
In engineering, zirconium is prized for nuclear applications because it is transparent to neutrons — invisible in the reactor, but structurally critical. Similarly, @periodic/zirconium works quietly at startup, invisible at runtime, yet structurally critical to every service it powers.
The name represents:
- Resilience: Validates everything once, fails fast before harm is done
- Precision: Strict type coercion with no silent misparses
- Transparency: Schema introspection and JSON Schema export
- Clarity: Explains what is wrong and where, not just that startup failed
Just as zirconium is an indispensable structural material in the right hands, @periodic/zirconium serves as the foundational configuration layer for production-grade Node.js applications.
🎯 Why Choose Zirconium?
Building robust backends requires trustworthy configuration from the very first line of code, but most solutions fall short:
dotenvalone reads files but does nothing to validate what's inside- Manual
process.envchecks are inconsistent, scattered, and always incomplete Boolean("false")silently returnstrue— bugs that only surface in production- Missing required vars crash apps mid-request instead of at startup
- No type safety means your IDE can't help you catch misconfigurations early
Periodic Zirconium provides the perfect solution:
✅ Zero dependencies — Pure TypeScript configuration core
✅ Framework-agnostic — Safe for use in both libraries and applications
✅ Strict Type Coercion — Prevents Boolean("false") === true and similar pitfalls
✅ Environment Scoping — Validate fields only in specific environments
✅ Secret Masking — Automatically masks sensitive values in logs and JSON output
✅ Deep Immutability — Frozen configuration prevents accidental mutations
✅ Schema Introspection — Query your configuration schema at runtime
✅ JSON Schema Export — Generate JSON Schema (Draft 2020-12) from your config
✅ Strict Mode — Detect undeclared environment variables
✅ Type-safe — Strict TypeScript from the ground up
✅ No global state — No side effects on import
✅ Production-ready — Fail fast at startup, never mid-request
📦 Installation
npm install @periodic/zirconiumOr with yarn:
yarn add @periodic/zirconiumOr with pnpm:
pnpm add @periodic/zirconium🚀 Quick Start
import { createConfig, string, number, boolean, enumType } from '@periodic/zirconium';
// 1. Define your schema
const config = createConfig({
server: {
port: number().int().min(1000).max(65535).default(4000),
env: enumType(['development', 'production', 'test']).default('development'),
},
database: {
url: string().from('DATABASE_URL').required(),
poolSize: number().int().min(1).max(50).default(10),
},
auth: {
jwtSecret: string().min(32).required().secret().onlyIn('production'),
},
features: {
enableRateLimit: boolean().default(true),
},
}, {
strict: true,
printSummary: true,
});
// 2. Use it — fully typed, deeply frozen, secrets masked
config.server.port; // number
config.auth.jwtSecret; // string
config.database.url; // stringExample validation error output:
============================================================
Invalid Environment Configuration:
============================================================
• auth.jwtSecret is required
• database.url must be at least 10 characters
• server.port must be at most 65535
• NODE_ENV must be one of: development, production, test
============================================================🧠 Core Concepts
The createConfig Function
createConfigis the primary factory function- Validates all environment variables once at startup
- Returns a deeply frozen, fully typed configuration object
- This is the main entry point for all applications
- No global state, safe for multi-tenant apps
Typical usage:
- Application code calls
createConfig()once at startup - Validation errors exit the process with a clear message
- The returned object is typed, frozen, and safe to pass anywhere
- Secrets are automatically masked in logs and JSON output
const config = createConfig({
port: number().int().min(1000).max(65535).default(4000),
secret: string().min(32).required().secret(),
}, {
strict: true,
printSummary: true,
});The Builder Pattern
- Field builders store metadata — no validation on definition
- Validation happens once — at
createConfig()call time - Builders are chainable — composable and readable
- Environment-aware — fields can be scoped to specific environments
Design principle:
Define the schema, call
createConfig, get a typed and trusted config object. Everything else is just usage.
// Schema-based routing to different validation rules
auth: {
jwtSecret: string().min(32).secret().required().onlyIn('production'),
devBypassKey: string().skipIn('production'),
},✨ Features
🔒 Secret Masking
Secrets are automatically masked in JSON output and console logs:
const config = createConfig({
apiKey: string().secret().required(),
publicKey: string().required(),
});
console.log(JSON.stringify(config));
// { "apiKey": "************", "publicKey": "pk_live_..." }❄️ Deep Immutability
Configuration is deeply frozen to prevent accidental mutations:
const config = createConfig({
server: { port: number().default(3000) },
});
config.server.port = 4000;
// ❌ TypeError: Cannot assign to read only property🎯 Environment Scoping
Validate fields only in the environments that need them:
const config = createConfig({
// Only required in production
jwtSecret: string().required().onlyIn('production'),
// Skipped in test environment
analytics: string().skipIn('test'),
// Required in production and staging
sslCert: string().required().onlyIn('production', 'staging'),
});📋 Strict Mode
Detect undeclared environment variables that have no business being there:
// .env
PORT=3000
UNKNOWN_VAR=value // Not declared in schema
const config = createConfig({
port: number().required(),
}, { strict: true });
// Error: UNKNOWN_VAR is not declared in schema🛡️ Strict Type Coercion
Zirconium uses strict parsers to prevent common runtime pitfalls:
// ✅ CORRECT — Zirconium boolean parsing
"true" → true
"false" → false
"1" → true
"0" → false
"yes" → Error ❌
// ❌ WRONG — JavaScript Boolean()
Boolean("false") === true // 😱
// ✅ CORRECT — Zirconium number parsing
"42" → 42
"3.14" → 3.14
"abc" → Error ❌
"" → Error ❌
// ❌ WRONG — JavaScript Number()
Number("") === 0 // 😱
Number(" ") === 0 // 😱🔍 Schema Introspection
Query your configuration schema at runtime:
import { getSchemaMetadata, getRequiredFields, getSecretFields } from '@periodic/zirconium';
const metadata = getSchemaMetadata(config);
// [
// {
// path: "auth.jwtSecret",
// type: "string",
// required: true,
// secret: true,
// envKey: "JWT_SECRET",
// onlyIn: ["production"]
// },
// ...
// ]
const required = getRequiredFields(config);
const secrets = getSecretFields(config);📄 JSON Schema Export
Generate JSON Schema (Draft 2020-12) from your config for tooling, docs, or validation pipelines:
import { exportJsonSchema } from '@periodic/zirconium';
const schema = exportJsonSchema(config);
console.log(JSON.stringify(schema, null, 2));Example output:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"server": {
"type": "object",
"properties": {
"port": {
"type": "number",
"minimum": 1000,
"maximum": 65535
},
"env": {
"type": "string",
"enum": ["development", "production", "test"]
}
},
"required": ["port", "env"]
}
},
"required": ["server"]
}📚 Common Patterns
1. Basic Express App
import express from 'express';
import { createConfig, string, number } from '@periodic/zirconium';
const config = createConfig({
server: {
port: number().int().min(1000).max(65535).default(3000),
host: string().default('0.0.0.0'),
},
database: {
url: string().from('DATABASE_URL').required(),
},
});
const app = express();
app.listen(config.server.port, config.server.host);2. Production Configuration with Secrets
const config = createConfig({
auth: {
jwtSecret: string().min(32).secret().required().onlyIn('production'),
jwtExpiry: number().default(86400),
},
external: {
stripeKey: string().secret().onlyIn('production'),
sendgridKey: string().secret().onlyIn('production'),
},
}, {
strict: true,
printSummary: true,
});3. Error Logging Integration
Send validation errors to your logging service before the app exits:
import * as Sentry from '@sentry/node';
import logger from './logger';
const config = createConfig(schema, {
onError: (errors) => {
Sentry.captureException(new Error('Configuration validation failed'), {
extra: { errors },
});
logger.error('Invalid configuration', {
errors: errors.map(e => ({
field: e.path,
message: e.message,
})),
});
},
});
// Process still exits with code 1 after onError runs4. Custom Thresholds per Environment
const isDevelopment = process.env.NODE_ENV === 'development';
const config = createConfig({
server: {
port: number().int().min(1000).max(65535).default(isDevelopment ? 3000 : 8080),
},
database: {
poolMax: number().int().default(isDevelopment ? 5 : 25),
},
});5. Structured Logging Integration
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
const config = createConfig(schema, {
onError: (errors) => logger.error('config.invalid', { errors }),
});6. Schema Introspection for Diagnostics
import { getSchemaMetadata, getRequiredFields, getSecretFields } from '@periodic/zirconium';
// Useful in health check endpoints or startup diagnostics
app.get('/health/config', (req, res) => {
res.json({
required: getRequiredFields(config).map(f => f.path),
secrets: getSecretFields(config).map(f => f.path),
});
});7. Multi-Service Monorepo
// packages/shared/config.ts
export const sharedConfig = createConfig({
database: {
url: string().from('DATABASE_URL').required(),
poolMax: number().int().default(10),
},
});
// apps/api/config.ts
export const apiConfig = createConfig({
server: {
port: number().int().default(3000),
},
auth: {
jwtSecret: string().min(32).secret().required().onlyIn('production'),
},
});8. Full Production Configuration
import { createConfig, string, number, boolean, enumType } from '@periodic/zirconium';
const isDevelopment = process.env.NODE_ENV === 'development';
export const config = createConfig({
server: {
port: number().int().min(1000).max(65535).default(4000),
host: string().default('0.0.0.0'),
env: enumType(['development', 'production', 'test']).default('development'),
},
database: {
url: string().from('DATABASE_URL').required(),
poolMin: number().int().min(0).default(2),
poolMax: number().int().min(1).default(10),
idleTimeout: number().default(30000),
},
redis: {
url: string().from('REDIS_URL').required(),
maxRetries: number().int().default(3),
},
auth: {
jwtSecret: string().min(32).secret().required().onlyIn('production'),
jwtExpiry: number().default(86400),
refreshTokenExpiry: number().default(604800),
},
features: {
enableRateLimit: boolean().default(true),
enableCaching: boolean().default(true),
enableMetrics: boolean().default(false).onlyIn('production'),
},
external: {
stripeKey: string().secret().onlyIn('production'),
sendgridKey: string().secret().onlyIn('production'),
},
}, {
strict: true,
printSummary: true,
});
export default config;🎛️ Configuration Options
createConfig Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| strict | boolean | false | Fail if undeclared env vars exist |
| printSummary | boolean | false | Print formatted config summary at startup |
| env | string | process.env.NODE_ENV | Custom environment override |
| onError | (errors) => void | — | Callback before process exit on validation failure |
const config = createConfig(schema, {
strict: true,
printSummary: true,
env: 'production',
onError: (errors) => logger.error('config.invalid', { errors }),
});Field Builder Options
| Method | Applies To | Description |
|--------|-----------|-------------|
| .required() | All | Field must be present |
| .default(value) | All | Default value if not set |
| .secret() | All | Mask in output and logs |
| .from("VAR") | All | Custom env var name |
| .onlyIn("env") | All | Only validate in specific environments |
| .skipIn("env") | All | Skip validation in specific environments |
| .min(n) | string, number | Minimum length or value |
| .max(n) | string, number | Maximum length or value |
| .int() | number | Must be an integer |
| .regex(pattern) | string | Pattern validation |
📋 API Reference
createConfig(schema, options?)
Validate and return a fully typed, deeply frozen configuration object.
function createConfig<T>(schema: T, options?: ConfigOptions): Resolved<T>Field Builders
string(): StringBuilder
number(): NumberBuilder
boolean(): BooleanBuilder
enumType(values: string[]): EnumBuilderIntrospection
getSchemaMetadata(config): FieldMetadata[]
getRequiredFields(config): FieldMetadata[]
getSecretFields(config): FieldMetadata[]
exportJsonSchema(config): JSONSchemaTypes
interface ConfigOptions {
strict?: boolean;
printSummary?: boolean;
env?: string;
onError?: (errors: ConfigError[]) => void | Promise<void>;
}
interface FieldMetadata {
path: string;
type: 'string' | 'number' | 'boolean' | 'enum';
required: boolean;
secret: boolean;
envKey: string;
onlyIn?: string[];
skipIn?: string[];
}
interface ConfigError {
path: string;
message: string;
}🧩 Architecture
@periodic/zirconium/
├── src/
│ ├── core/ # Validation and processing engine
│ │ ├── create-config.ts # Main createConfig factory
│ │ ├── validator.ts # Schema and field validation logic
│ │ ├── parsers.ts # Strict type coercion engine
│ │ ├── freeze.ts # Deep immutability
│ │ ├── masking.ts # Secret value masking
│ │ └── printer.ts # Config summary printer
│ ├── builders/ # Field builder implementations
│ │ ├── string.ts # String builder
│ │ ├── number.ts # Number builder
│ │ ├── boolean.ts # Boolean builder
│ │ ├── enum.ts # Enum builder
│ │ └── index.ts # Re-exports all builders
│ ├── introspection/ # Schema introspection and JSON Schema export
│ │ └── index.ts # getSchemaMetadata, getRequiredFields, exportJsonSchema
│ ├── json-schema/ # JSON Schema (Draft 2020-12) generation
│ │ └── index.ts # exportJsonSchema
│ ├── types.ts # TypeScript interfaces and type inference
│ └── index.ts # Public APIDesign Philosophy:
- Core is pure TypeScript with no dependencies
- Builders store metadata declaratively without side effects
- Validation happens exactly once, at startup, with full context
- Introspection exposes the schema for tooling and diagnostics
- Easy to extend with custom field types
📈 Performance
Zirconium is optimized for startup-time validation and zero-overhead runtime access:
- Fail fast — Validates all environment variables once at process start
- Zero runtime cost — No validation logic runs after
createConfigreturns - Deep freeze — Prevents mutation without runtime overhead
- Async
onError— Supports async error reporting before process exit - No monkey-patching — Clean, predictable behavior throughout
🚫 Explicit Non-Goals
This package intentionally does not include:
❌ Runtime re-validation after startup
❌ Watching .env files for changes
❌ Vendor-specific lock-in of any kind
❌ Built-in dotenv file loading (use dotenv for that)
❌ Magic or implicit behavior on import
❌ Configuration merging from multiple sources
❌ Metrics or tracing (use @periodic/arsenic for that)
❌ Configuration files (configure in code)
Focus on doing one thing well: strict, typed, production-safe environment configuration.
🎨 TypeScript Support
Full TypeScript support with complete type inference:
import type {
ConfigOptions,
FieldMetadata,
ConfigError,
} from '@periodic/zirconium';
const config = createConfig({
port: number().int().default(3000),
secret: string().secret().required(),
});
// TypeScript knows the exact shape
config.port; // number
config.secret; // string🧪 Testing
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watchNote: All tests achieve >80% code coverage.
🤝 Related Packages
Part of the Periodic series by Uday Thakur:
- @periodic/iridium - Structured logging
- @periodic/arsenic - Semantic runtime monitoring
- @periodic/obsidian - HTTP error handling
- @periodic/titanium - Rate limiting
- @periodic/osmium - Redis caching
Build complete, production-ready APIs with the Periodic series!
📖 Documentation
🛠️ Production Recommendations
Environment Variables
NODE_ENV=production
DATABASE_URL=postgres://...
JWT_SECRET=a-very-long-secret-at-least-32-charactersLog Aggregation
Pair with @periodic/iridium for structured JSON output:
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
import { createConfig } from '@periodic/zirconium';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
const config = createConfig(schema, {
onError: (errors) => logger.error('config.invalid', { errors }),
});
// Pipe to Elasticsearch, Datadog, CloudWatch, etc.Error Monitoring
Integrate with error tracking:
const config = createConfig(schema, {
onError: (errors) => {
Sentry.captureEvent({
message: 'Configuration validation failed',
extra: { errors },
});
},
});📝 License
MIT © Uday Thakur
🙏 Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details on:
- Code of conduct
- Development setup
- Pull request process
- Coding standards
- Architecture principles
📞 Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
🌟 Show Your Support
Give a ⭐️ if this project helped you build better applications!
Built with ❤️ by Uday Thakur for production-grade Node.js applications
