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

推荐订阅源

V
V2EX
C
Check Point Blog
博客园_首页
B
Blog
D
Docker
U
Unit 42
量子位
I
InfoQ
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
GbyAI
GbyAI
L
LangChain Blog
云风的 BLOG
云风的 BLOG
博客园 - Franky
美团技术团队
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
月光博客
月光博客
Vercel News
Vercel News
Recent Announcements
Recent Announcements
雷峰网
雷峰网
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Google DeepMind News
Google DeepMind News

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
Stop Using process.env Directly — Here's Why
Odejobi Abiola Samuel · 2026-06-18 · via DEV Community

Last month I pushed a new service to staging. Forgot to add REDIS_URL to the env file. The server started fine — no crash, no error — but the first request that tried to hit the cache hung forever. Took me 20 minutes and a strace to figure out the connection was silently failing.

I've done this enough times now to recognize the pattern: environment variables in Node.js are just string | undefined. TypeScript can't help you. Validation is your job. Most teams do it wrong, or don't do it at all.

The Problem with process.env

const dbUrl = process.env.DATABASE_URL
//    ^? string | undefined — TypeScript can't help you here
const port = process.env.PORT
//    ^? string | undefined — also a string, not a number

Three things go wrong here:

  1. No guarantee the variable exists. It could be undefined at any time, and you won't know until runtime.
  2. No type information. PORT is a number semantically, but it's a string at runtime. Every consumer has to parse it themselves.
  3. No documentation. What format should DATABASE_URL be? What's the default for PORT? Who knows — go find the README, which is probably outdated.

Three Common Failure Patterns

1. Missing Required Vars

// src/db.ts
const pool = new Pool({
  connectionString: process.env.DATABASE_URL, // undefined — oops
})
// Error: connect ECONNREFUSED — good luck debugging that

No error at import time. No error at server start. The error surfaces the first time someone hits a database endpoint.

2. Wrong Format, Cryptic Errors

// .env
DATABASE_URL=localhost:5432/myapp  // forgot the postgres:// prefix

// src/db.ts
const url = new URL(process.env.DATABASE_URL) // TypeError: Invalid URL

Or worse — silent data corruption:

// .env
MAX_CONNECTIONS=not-a-number

// src/config.ts
const max = parseInt(process.env.MAX_CONNECTIONS ?? "10", 10)
//    ^? NaN — your connection pool silently uses NaN as max

3. Type Confusion

// .env
PORT=3000

// src/server.ts
const port = process.env.PORT // "3000" — it's a string!
app.listen(port + 1)          // listens on "30001", not 3001

Every parseInt, Number(), === "true", or as string is validation logic you're scattering across your codebase. Most teams don't even do that.

The Manual Approach

I've written this function before:

// src/env.ts
function getEnv(): Env {
  const databaseUrl = process.env.DATABASE_URL
  if (!databaseUrl) throw new Error("DATABASE_URL is required")

  const port = parseInt(process.env.PORT ?? "3000", 10)
  if (isNaN(port) || port < 1 || port > 65535) {
    throw new Error("PORT must be between 1 and 65535")
  }

  const nodeEnv = process.env.NODE_ENV ?? "development"
  if (!["development", "production", "test"].includes(nodeEnv)) {
    throw new Error(`Invalid NODE_ENV: ${nodeEnv}`)
  }

  const jwtSecret = process.env.JWT_SECRET
  if (!jwtSecret) throw new Error("JWT_SECRET is required")
  if (jwtSecret.length < 32) throw new Error("JWT_SECRET must be at least 32 chars")

  return { databaseUrl, port, nodeEnv, jwtSecret } as const
}

export const env = getEnv()

It works. But it's repetitive, scattered, undocumented, not CI-friendly, and TypeScript can't infer literal types from it.

Schema-Based Validation

What if you could write this instead:

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

const env = defineEnv({
  DATABASE_URL: string().url().describe("PostgreSQL connection URL"),
  PORT: number().port().default(3000),
  NODE_ENV: pick(["development", "production", "test"] as const).default("development"),
  JWT_SECRET: string().secret().min(32).describe("JWT signing secret"),
})

And get all of this automatically:

  • TypeScript infers everythingenv.PORT is number, env.NODE_ENV is "development" | "production" | "test"
  • Validation at definition time — if DATABASE_URL is missing or invalid, it throws immediately
  • DefaultsPORT defaults to 3000 if not set
  • Secrets markedJWT_SECRET is flagged, won't leak into docs or logs
  • Descriptions — each variable carries its docs alongside the schema

How It Works

defineEnv() reads from your source (defaults to process.env), runs each value through its validator chain, and collects errors. If any exist, it throws CtroEnvError with every error grouped and formatted:

 ● Missing required (1)

   DATABASE_URL  Add this variable to your .env file or set it in the environment.

 ✗ Invalid (1)

   CORS_ORIGIN
   Invalid URL

No hunting through logs. You know exactly what's wrong and what to fix.

Validators at a Glance

Factory Creates Coercion
string() A string validator None
number() A number validator Coerces "3000" -> 3000, rejects NaN
boolean() A boolean validator Coerces "true"/"1" -> true, "false"/"0" -> false
pick([...]) A union of allowed string values Fuzzy suggestion on typo

String refinements: .url(), .email(), .port(), .min(n), .max(n), .regex(p)
Number refinements: .int(), .positive(), .port(), .min(n), .max(n)
Chainable (all validators): .default(v), .optional(), .describe(t), .secret(), .validate(fn)

Putting It Together

// src/env.ts
import { defineEnv, string, number, pick } from "@ctroenv/core"
import { loadEnv } from "@ctroenv/node"

const env = defineEnv({
  PORT: number().port().default(3000),
  HOST: string().default("0.0.0.0"),
  DATABASE_URL: string().url().describe("PostgreSQL connection URL"),
  JWT_SECRET: string().secret().min(32).describe("JWT signing secret"),
  CORS_ORIGIN: string().url().describe("Allowed CORS origin"),
  NODE_ENV: pick(["development", "production", "test"] as const).default("development"),
  LOG_LEVEL: pick(["debug", "info", "warn", "error"] as const).default("info"),
  REDIS_URL: string().url().optional().describe("Redis connection URL"),
}, { source: loadEnv() })

export { env }

// src/index.ts
import express from "express"
import { env } from "./env"

const app = express()

app.get("/health", (_req, res) => {
  res.json({ status: "ok", uptime: process.uptime(), nodeEnv: env.NODE_ENV, port: env.PORT })
})

app.listen(env.PORT, env.HOST, () => {
  console.log(`Server running on http://${env.HOST}:${env.PORT}`)
})

If any env var is missing or invalid, the app fails at import time — not the first time someone hits a route.

Beyond Validation

Once your schema exists, the CLI tools layer on:

  • npx ctroenv generate — creates .env.example from your schema
  • npx ctroenv validate — fails CI if env drifts from schema
  • npx ctroenv docs — generates ENVIRONMENT.md
  • Secrets marked with .secret() are masked in output and commented out in .env.example
Approach Type safety Error clarity Auto-docs CI support Lines per var
Raw process.env None None No No 1
Manual validation Partial Depends No No 5-10
CtroEnv Full Grouped Yes Yes 1

Try It

npm install @ctroenv/core

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

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

Resources: Docs · GitHub · npm

Next: Define Once, Trust Everywhere — CtroEnv Deep Dive