DEV Community

Cover image for dotenv loads your .env — it doesn't check it. So I built a typed validator.
benjamin
benjamin

Posted on

dotenv loads your .env — it doesn't check it. So I built a typed validator.

A PORT=8O80 typo (that's a letter O), a DATABASE_URL you forgot to set, a NODE_ENV=prodd — none of these fail when your app starts. They fail later: a cryptic stack trace three layers into startup, a service that boots but talks to the wrong database, a feature flag that's silently off in production. The error is never "your env is wrong"; it's whatever broke downstream.

dotenv loads your .env. It doesn't check it. So I built envward: validate the whole environment against a small typed schema up front, and fail loudly — with the actual problem — before a single line of app code runs. Zero dependencies, no network.

$ envward

.env — checked against env.schema.json

  ✗ API_KEY   missing — required string
  ✗ NODE_ENV  "prodd" is not one of: development, production, test
  ✗ PORT      70000 is above max 65535

3 problem(s), 3 key(s) valid
Enter fullscreen mode Exit fullscreen mode

It exits non-zero on any problem, so it drops straight into a prestart hook or a CI step.

Get a schema in one command

No hand-writing JSON from scratch:

envward --init > env.schema.json
Enter fullscreen mode Exit fullscreen mode

--init reads your existing .env and guesses a type for each key (8080int, https://…url, truebool, …), marking them required. Then you tighten it:

{
  "PORT":         { "type": "int", "required": true, "min": 1, "max": 65535 },
  "DATABASE_URL": { "type": "url", "required": true },
  "NODE_ENV":     { "type": "enum", "values": ["development", "production", "test"] },
  "API_KEY":      { "type": "string", "required": true, "minLength": 16 }
}
Enter fullscreen mode Exit fullscreen mode

Types: string (with minLength/maxLength/pattern), int / number (with min/max), bool, url, email, enum. An empty value (KEY=) counts as missing.

How it's different from a drift checker

A .env drift tool tells you which keys are missing versus .env.example. envward validates the values: is PORT actually an integer in range, is DATABASE_URL actually a URL, is NODE_ENV one of the allowed set. Different failure mode, caught at a different time.

Install

npx envward          # Node
pip install envward  # Python — same behavior
Enter fullscreen mode Exit fullscreen mode

Two builds (Node + Python) that validate identically, so it fits whatever your stack already runs.

Use it as a gate

# package.json: "prestart": "envward"   — refuse to boot with a broken .env
# CI:           envward --env .env.ci --strict
Enter fullscreen mode Exit fullscreen mode

--strict also flags keys present in .env but missing from the schema.

A couple of honest notes

  • Zero dependencies, both builds — stdlib only.
  • A malformed schema is an error, not a guess. A non-numeric min, a bad regex pattern, an unknown type — envward exits 2 with a clear message in both builds, rather than crashing or silently passing. (Getting Node and Python to agree on every edge here took a real adversarial pass.)
  • pattern is matched in ASCII mode and as an unanchored search — wrap it in ^…$; keep to a portable regex subset for identical behavior across both builds.

Links


How do you guard environment config today — a hand-rolled startup check, a framework feature, or just hope? And would you gate CI on it?

Top comments (1)

Collapse
 
theoephraim profile image
Theo Ephraim

Use varlock.dev - it’s a mature solution that solves this and many other features around config. Open source with an active user base.