惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
雷峰网
雷峰网
S
SegmentFault 最新的问题
量子位
有赞技术团队
有赞技术团队
V
V2EX
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Jina AI
Jina AI
C
Check Point Blog
G
Google Developers Blog
博客园 - 叶小钗
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园_首页
T
Tailwind CSS Blog
B
Blog RSS Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
酷 壳 – CoolShell
酷 壳 – CoolShell
U
Unit 42

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How to Validate Environment Variables Without a Library (...
Odejobi Abiola Samuel · 2026-06-24 · via DEV Community

How to Validate Environment Variables Without a Library (And Why You Should Anyway)

I'm a fan of minimal dependencies. Every library you add is code you don't control, surface area for bugs, and another thing to keep up to date. So when I started a new project last month, I told myself: no env validation library. I'll write it myself. It's just a few vars.

Six hours later I had a surprisingly solid little validation function. Let me show you what I built.

Part 1: Your Own Env Validation in Plain TypeScript

Here's the complete thing, about 60 lines:

type EnvSchema = Record<string, {
  type: "string" | "number" | "boolean"
  required?: boolean
  default?: unknown
}>

function loadEnv<T extends EnvSchema>(
  schema: T,
  source: Record<string, string | undefined> = process.env
): { [K in keyof T]: T[K]["type"] extends "number" ? number
     : T[K]["type"] extends "boolean" ? boolean
     : string } {

  const result: Record<string, unknown> = {}

  for (const [key, config] of Object.entries(schema)) {
    let raw = source[key]

    if (raw === undefined) {
      if (config.default !== undefined) {
        raw = String(config.default)
      } else if (config.required !== false) {
        throw new Error(`Missing required env var: ${key}`)
      } else {
        continue
      }
    }

    switch (config.type) {
      case "number": {
        const num = Number(raw)
        if (Number.isNaN(num)) {
          throw new Error(`${key} must be a number, got "${raw}"`)
        }
        result[key] = num
        break
      }
      case "boolean": {
        const truthy = ["true", "yes", "1", "on"]
        const falsy = ["false", "no", "0", "off"]
        if (truthy.includes(raw.toLowerCase())) {
          result[key] = true
        } else if (falsy.includes(raw.toLowerCase())) {
          result[key] = false
        } else {
          throw new Error(`${key} must be a boolean, got "${raw}"`)
        }
        break
      }
      default:
        result[key] = raw
    }
  }

  return result as any
}

Usage:

const env = loadEnv({
  PORT: { type: "number", default: 3000 },
  DATABASE_URL: { type: "string", required: true },
  DEBUG: { type: "boolean", default: false },
})

env.PORT // number

That works. It's typed. It validates at startup. I was pretty proud of this for about a day.

Part 2: Why You Might Want a Library Anyway

Then the project grew. Here's what happened:

Your validation function grows. You start adding refinements: "PORT must be between 1024 and 65535", "DATABASE_URL must start with postgres://". Now your schema config has extra fields and your validation loop has special cases.

Error messages are inconsistent. I wrote decent errors, but what about when someone else touches the file? Every team member writes messages differently. Some are helpful. Some say "invalid". Good luck debugging that in CI.

No CLI tooling. Want to validate .env.production before a deploy? You're running a script that imports your config module. You can't just point a CLI at an env file and check it against your schema.

No documentation generation. I kept a ENVIRONMENT.md file updated for about three days. Then it was stale forever. Nobody wants to manually document env vars.

No framework adapters. Vite uses import.meta.env. Next.js inlines vars at build time. If your config module assumes process.env, it doesn't work everywhere. You end up maintaining adapters yourself.

No secret masking. One accidental console.log(config) later, your entire team has new API keys to rotate.

Part 3: What CtroEnv Does Differently

I'm not here to tell you CtroEnv is the only answer. But since I built it, let me show you what I mean by "a library handles this."

Same validation from Part 1:

import { defineEnv, string, number, boolean } from "@ctroenv/core"

const env = defineEnv({
  PORT: number().port().default(3000),
  DATABASE_URL: string().url(),
  DEBUG: boolean().default(false),
})

Same type safety. Fewer lines. But the real difference isn't in the code — it's everything around it.

CLI validation: npx ctroenv validate --source .env.production — checks your env against the schema and exits with a non-zero code on failure. Drop it in your CI pipeline.

Generated docs: npx ctroenv docs produces an ENVIRONMENT.md file that's always accurate because it's generated from the schema.

Secret masking: Add .secret() to any variable and it's hidden from logs, console output, and JSON.stringify.

Framework adapters: One schema, but it works with process.env, import.meta.env, and Next.js's build-time inlining without changing your code.

Consistent errors: Every validation failure follows the same format, with error codes you can check programmatically.

So Should You Use a Library?

If you have 3 env vars in a personal project, write your own function. You'll learn something and you won't need the extras.

If you have a team, a CI pipeline, staging and production deploys, and more than 5 env vars — use a library. The validation logic is the easy part. It's the tooling, the docs, the edge cases, and the framework support that'll eat your time.


Links: GitHub | npm | Docs