🪟 Windows TippsAndroid 17: Neue Version ist hier – Das ist alles neu(16.09.2026 um 11:40 Uhr)
🕵️ Hacking12 Best CASB Solutions Compared (2026): Features & Pricing(16.09.2026 um 09:31 Uhr)
🕵️ Hacking12 Best CIEM Tools Compared (2026): Features & Pricing(16.09.2026 um 09:37 Uhr)
🪟 Windows TippsAndroid 17: Neue Version ist hier – Das ist alles neu(16.09.2026 um 11:40 Uhr)
🕵️ Hacking12 Best CASB Solutions Compared (2026): Features & Pricing(16.09.2026 um 09:31 Uhr)
🕵️ Hacking12 Best CIEM Tools Compared (2026): Features & Pricing(16.09.2026 um 09:37 Uhr)

🔧 Programmierung 🕛 vor 3 Monaten 13 Min Lesezeit
0

Stop your app from booting with broken env vars: a type-safe, universal config library

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

TL;DRprocess.env.PORT is a string | undefined, your bundler silently inlines client env at build time, and every runtime exposes env differently. @teispace/env is a zero-dependency library that loads, validates, coerces, and types your environment once and works the same on Node, Bun, Deno, Cloudflare Workers, Next.js, NestJS, Vite, Nuxt, Astro, and SvelteKit. Bring Zod/Valibot/ArkType, or use the built-in coercers. npm i @teispace/env.





CODE
import { defineEnv, e } from '@teispace/env';

export const env = defineEnv({
schema: {
NODE_ENV: e.enum(['development', 'production', 'test']).default('development'),
PORT: e.port().default(3000),
DATABASE_URL: e.url(),
ENABLE_CACHE: e.boolean().default(false),
},
});

env.PORT; // number ← coerced AND typed (not the string "3000")
env.DATABASE_URL; // string
env.ENABLE_CACHE; // boolean






If a variable is missing or malformed, your app fails fast at boot with one clear error — not three layers deep at 2 a.m. with a cryptic undefined.









The problem nobody admits is a problem



Environment variables feel trivial. You add dotenv, read process.env.WHATEVER, ship it. Then reality arrives:






1. process.env lies to TypeScript



Every value on process.env is typed string | undefined. So this compiles and explodes at runtime:




CODE
const port = process.env.PORT;     // string | undefined
server.listen(port); // expected a number… 🙃
const retries = process.env.RETRIES * 2; // NaN, silently






You can augment NodeJS.ProcessEnv to claim PORT: number, but that's a lie: declaration merging tells TypeScript what should exist, not what does, and process.env.PORT is still the string "3000" at runtime. The type and the value disagree — the worst kind of bug.






2. The "transform" trap



Libraries like t3-env validate and coerce into a returned object. Great — if you read that object. But the moment a teammate reads raw process.env.PORT after a coercion, the type says number and the value is still a string. The type lies again, just one level removed.






3. Your bundler rewrites env behind your back



This is the one that eats afternoons. In Vite, import.meta.env.VITE_X is statically replaced at build time — and only for literal, VITE_-prefixed keys. In Next.js, process.env.NEXT_PUBLIC_X is inlined into the browser bundle at build. So:




CODE
// ❌ On the client, this is `undefined` after bundling:
const key = 'VITE_API_URL';
const url = import.meta.env[key]; // dynamic key → not statically replaced






A naive getEnv(key) helper that reads a dynamic key works on the server and silently returns undefined in the browser. The bundler can only see literal access.






4. Every runtime exposes env differently






































Runtime Where env lives Global?
Node / Bun process.env
Deno
Deno.env.get() (or process.env in compat)
Cloudflare Workers the env binding passed into your handler
no global at all
Vite client
import.meta.env.VITE_* (build-time replaced)
⚠️ build-time only
Next client
process.env.NEXT_PUBLIC_* (build-time inlined)
⚠️ build-time only


Write a helper that reads process.env and it crashes on Workers. Write one for Workers and it's awkward everywhere else.






5. Secrets leak into client bundles



Forget the NEXT_PUBLIC_/VITE_ prefix discipline once and your STRIPE_SECRET ships to every browser. There's usually nothing stopping you.



The existing tools each solve part of this:






























































dotenv @t3-oss/env envalid
Loads .env
Validates + coerces + types
Types never lie ⚠️ (via raw process.env)
Bring any validator ⚠️ (Zod/Std Schema)
Built-in coercers (zero dep)
Client/server leak guard
Universal runtime (incl. Workers) ⚠️
Dependencies 0 0 (needs a validator) 1


Nobody does all of it in one lean package. That gap is @teispace/env.









Install






CODE
npm i @teispace/env
# pnpm add @teispace/env · yarn add @teispace/env · bun add @teispace/env · deno add npm:@teispace/env






ESM-only. Requires Node ≥ 22.12 (or Bun / Deno / Workers). Zero runtime dependencies.









The core idea: one validated, coerced, frozen object



defineEnv reads your environment, validates each variable, coerces it to the right type, and returns a frozen object that is the single source of truth:




CODE
import { defineEnv, e } from '@teispace/env';

export const env = defineEnv({
schema: {
NODE_ENV: e.enum(['development', 'production', 'test']).default('development'),
PORT: e.port().default(3000),
DATABASE_URL: e.url(),
ENABLE_CACHE: e.boolean().default(false),
},
});









CODE
env.PORT;          // 3000 — a real `number`
typeof env.PORT; // "number"
env.ENABLE_CACHE; // false — a real `boolean`
Object.isFrozen(env); // true






Because the value was actually coerced (not type-asserted), the type and the runtime value can never disagree. Read env.* everywhere; never touch process.env again. This is the "types never lie" guarantee — and it's the headline difference from approaches that only fix the type.




The output type is fully inferred from your schema. env.PORT is number, env.NODE_ENV is 'development' | 'production' | 'test', env.DATABASE_URL is string — no z.infer, no manual interface, no as.










Built-in coercers (e.*)



Each coercer turns the raw string | undefined into a typed, validated value. Constraints are passed as options; behaviors are chained:




CODE
e.string({ min, max, regex, startsWith, endsWith });
e.number({ min, max, int });
e.int({ min, max });
e.port(); // 1–65535
e.boolean(); // true/1/yes/on vs false/0/no/off/""
e.url({ protocol }); // WHATWG URL; e.urlObject() returns a URL instance
e.email();
e.enum(['a', 'b', 'c']); // narrows to 'a' | 'b' | 'c'
e.json<T>(innerSchema?); // JSON.parse + optional shape validation
e.array({ separator, trim, of }); // "a,b,c" → string[] (or coerced items via `of`)
e.host();
e.hostname();






Chainable on every coercer — each one narrows the type precisely:




CODE
e.string().optional();             // string | undefined
e.port().default(3000); // number (and no longer optional)
e.string({ min: 1 }).secret(); // value redacted in any error output
e.number().refine((n) => n % 2 === 0, 'must be even');
e.string().transform((s) => s.toUpperCase());
e.url().describe('Public API base URL');






A small but real example mixing several:




CODE
const env = defineEnv({
schema: {
EVEN_WORKERS: e.number({ int: true }).refine((n) => n % 2 === 0, 'must be even'),
ALLOWED_ORIGINS: e.array({ of: e.url() }), // "https://a.com,https://b.com" → string[]
LOG_LEVEL: e.enum(['debug', 'info', 'warn', 'error']).default('info'),
SERVICE_NAME: e.string().transform((s) => s.toLowerCase()),
},
});






env.ALLOWED_ORIGINS is string[], env.LOG_LEVEL is the union, and each origin is itself URL-validated.









Bring your own validator (Standard Schema)



Already invested in Zod, Valibot, or ArkType? They all implement .

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Build Anything with DeepSeek V4.1 Flash, Here's How..
1 Quelle
Followership, CyberSecurity Leadership, and Judgement as a Defining Skill - BSW #465
1 Quelle
Amazon Blitzangebote: MacBook Neo, Powerbanks, EcoFlow + Zendure, Mähroboter und mehr
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Stop your app from booting with broken env vars: a type-safe, universal config library

Thematisch verwandte Begriffe: Stop, your, from, booting · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...