DEV Community

SATINATH MONDAL
SATINATH MONDAL

Posted on

envguard: A Better Way to Validate Environment Variables in Node.js

Introducing EnvGuard - a zero-dependency, type-safe environment variable validator with features like secret masking, warning mode, and .env.example generation

We've all been there. Your application crashes in production with a cryptic error, and after hours of debugging, you discover it's because someone forgot to set an environment variable. Or worse, they set PORT=three-thousand instead of PORT=3000.

Today, I'm excited to introduce EnvGuard - a zero-dependency, type-safe environment variable validator for Node.js that catches these issues at startup, not at 3 AM.

The Problem with Environment Variables

Environment variables are the standard way to configure applications, but they come with challenges:

// The old way - error-prone and repetitive
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
const apiKey = process.env.API_KEY; // undefined? empty string? who knows!
const debug = process.env.DEBUG === 'true'; // what about 'yes', '1', 'on'?
Enter fullscreen mode Exit fullscreen mode

This approach has several problems:

  • No validation until the value is actually used
  • Manual type coercion everywhere
  • No TypeScript type inference
  • Sensitive values can leak into logs
  • No documentation of required variables

Enter EnvGuard

EnvGuard solves all of these problems with a clean, declarative API:

import { cleanEnv, str, num, bool, url } from '@opensourcesforge/envguard';

const env = cleanEnv({
  PORT: num({ default: 3000 }),
  DATABASE_URL: url(),
  API_KEY: str({ secret: true }),
  DEBUG: bool({ default: false }),
});

// Full TypeScript inference!
console.log(env.PORT);        // number
console.log(env.DATABASE_URL); // string (validated URL)
console.log(env.API_KEY);      // string (masked in errors)
console.log(env.DEBUG);        // boolean

// Built-in environment helpers
if (env.isProduction) {
  enableCaching();
}
Enter fullscreen mode Exit fullscreen mode

What Makes EnvGuard Different?

I built EnvGuard because existing solutions were missing features I needed. Here's what sets EnvGuard apart:

1. Zero Dependencies

EnvGuard has no runtime dependencies. Your node_modules stays lean, and you don't inherit security vulnerabilities from transitive dependencies.

2. Secret Masking

When validation fails, sensitive values are automatically masked:

const env = cleanEnv({
  API_KEY: str({ secret: true }),
});

// If API_KEY is invalid, error shows:
// ERROR: API_KEY - Invalid value (received: "sk_l****_xxx")
Enter fullscreen mode Exit fullscreen mode

No more accidentally exposing secrets in CI logs!

3. Test-Specific Defaults

Unlike other libraries that only have devDefault, EnvGuard supports separate defaults for test environments:

const env = cleanEnv({
  DATABASE_URL: url({
    testDefault: 'postgres://localhost/test_db',  // NODE_ENV=test
    devDefault: 'postgres://localhost/dev_db',    // NODE_ENV=development
    // Required in production
  }),
});
Enter fullscreen mode Exit fullscreen mode

4. Warning Mode

Not every missing variable should crash your app. Use warnOnly for optional features:

const env = cleanEnv({
  // Critical - will fail if missing
  DATABASE_URL: url(),

  // Optional - logs warning but continues
  ANALYTICS_ID: str({ 
    warnOnly: true,
    desc: 'Google Analytics ID (optional)',
  }),
});
Enter fullscreen mode Exit fullscreen mode

5. Extra Variable Detection

Catch typos before they cause problems:

const env = cleanEnv(
  { PORT: num() },
  { warnOnExtra: true }
);

// If PROT=3000 is set (typo), you'll see:
// WARNING: PROT - Unknown environment variable
Enter fullscreen mode Exit fullscreen mode

6. Conditional Requirements

Sometimes a variable is only required based on other configuration:

const env = cleanEnv({
  USE_SMTP: bool({ default: false }),

  SMTP_HOST: str({
    requiredWhen: (env) => env.USE_SMTP === true,
  }),

  SMTP_PASSWORD: str({
    requiredWhen: (env) => env.USE_SMTP === true,
    secret: true,
  }),
});
Enter fullscreen mode Exit fullscreen mode

7. Rich Validator Library

EnvGuard includes validators you won't find elsewhere:

import { 
  str, num, bool,           // Basics
  url, email, host, port,   // Network
  json, array, uuid,        // Data
  duration, bytes,          // Special
  enums, regex,             // Validation
  makeValidator,            // Custom
} from '@opensourcesforge/envguard';

const env = cleanEnv({
  // Parse duration strings
  CACHE_TTL: duration({ default: 300000 }), // '5m' -> 300000ms

  // Parse byte sizes
  MAX_UPLOAD: bytes({ unit: 'MB' }), // '50MB' -> 50

  // Comma-separated arrays
  ALLOWED_ORIGINS: array({ default: ['localhost'] }),

  // Type-safe enums
  LOG_LEVEL: enums({ 
    values: ['debug', 'info', 'warn', 'error'] as const,
    default: 'info',
  }),
});
Enter fullscreen mode Exit fullscreen mode

8. Auto-Generate .env.example

Document your environment variables automatically:

import { writeEnvExample, str, num, url } from '@opensourcesforge/envguard';

const spec = {
  PORT: num({ default: 3000, desc: 'Server port' }),
  DATABASE_URL: url({ desc: 'PostgreSQL connection URL' }),
  API_KEY: str({ desc: 'API authentication key', secret: true }),
};

writeEnvExample(spec);
Enter fullscreen mode Exit fullscreen mode

Generates:

# Server port
PORT=3000

# PostgreSQL connection URL
DATABASE_URL=

# API authentication key
API_KEY=
Enter fullscreen mode Exit fullscreen mode

Real-World Example

Here's how I use EnvGuard in a typical Express application:

// src/env.ts
import { cleanEnv, str, num, bool, url, email, duration } from '@opensourcesforge/envguard';

export const env = cleanEnv({
  // Server
  PORT: num({ default: 3000, desc: 'HTTP server port' }),
  HOST: str({ default: '0.0.0.0' }),

  // Database
  DATABASE_URL: url({ secret: true }),
  DB_POOL_SIZE: num({ default: 10 }),

  // Redis
  REDIS_URL: url({ devDefault: 'redis://localhost:6379' }),
  REDIS_TTL: duration({ default: 3600000 }), // 1 hour

  // Auth
  JWT_SECRET: str({ secret: true }),
  JWT_EXPIRES_IN: str({ default: '7d' }),

  // Email (optional)
  SMTP_HOST: str({ warnOnly: true }),
  SMTP_PORT: num({ default: 587 }),
  FROM_EMAIL: email({ default: 'noreply@example.com' }),

  // Features
  ENABLE_SWAGGER: bool({ default: true }),
  LOG_LEVEL: str({ 
    choices: ['debug', 'info', 'warn', 'error'],
    default: 'info',
  }),
});

// src/app.ts
import express from 'express';
import { env } from './env';

const app = express();

if (env.ENABLE_SWAGGER && !env.isProduction) {
  // Setup Swagger
}

app.listen(env.PORT, env.HOST, () => {
  console.log(`Server running on ${env.HOST}:${env.PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Getting Started

Install EnvGuard:

npm install @opensourcesforge/envguard
Enter fullscreen mode Exit fullscreen mode

Create your environment configuration:

// src/env.ts
import { cleanEnv, str, num, bool } from '@opensourcesforge/envguard';

export const env = cleanEnv({
  NODE_ENV: str({ choices: ['development', 'test', 'production'] }),
  PORT: num({ default: 3000 }),
  DEBUG: bool({ default: false }),
});
Enter fullscreen mode Exit fullscreen mode

That's it! Your environment variables are now validated at startup with full TypeScript support.

Migration from envalid

If you're using envalid, migration is straightforward:

- import { cleanEnv, str, num } from 'envalid';
+ import { cleanEnv, str, num } from '@opensourcesforge/envguard';

- const env = cleanEnv(process.env, {
+ const env = cleanEnv({
    PORT: num({ default: 3000 }),
    API_KEY: str(),
  });
Enter fullscreen mode Exit fullscreen mode

Links

Conclusion

Environment variable validation shouldn't be an afterthought. With EnvGuard, you get:

  • Fail-fast validation at startup
  • Full TypeScript type inference
  • Secret masking for sensitive values
  • Flexible defaults for different environments
  • Zero dependencies

Give it a try and let me know what you think! I'd love to hear your feedback and feature requests.


EnvGuard is open source under the MIT license. Contributions are welcome!

Top comments (0)