Every project I start ends up with the same file: a zod schema for env vars, copied from the last project and slowly falling out of sync with .env. So I made type-envgen, which generates that file for you.
You write your .env as usual. Where you want more than the default, add a comment tag above the key:
```bash
NODE_ENV=development
@type url
DATABASE_URL=postgres://localhost:5432/app
@min 1
@max 65535
PORT=3000
DEBUG=false
@type enum(debug, info, warn, error)
@default info
LOG_LEVEL=info
@min 32
JWT_SECRET=change-me-change-me-change-me-123
@optional
SENTRY_DSN=
```
Run npx type-envgen and you get this:
```ts
import { z } from "zod";
import { validateEnv } from "type-envgen";
export const envSchema = z.object({
DATABASE_URL: z.url(),
DEBUG: z.stringbool(),
JWT_SECRET: z.string().min(32),
LOG_LEVEL: z.enum(["debug","info","warn","error"]).default("info"),
NODE_ENV: z.enum(["development","production","test"]),
PORT: z.coerce.number().int().min(1).max(65535),
SENTRY_DSN: z.string().optional(),
});
export const env = validateEnv(envSchema, process.env);
export type Env = z.infer<typeof envSchema>;
```
env.PORT is now a number and env.DEBUG is a boolean. If something is wrong, the app fails at startup with one readable list instead of crashing three files deep later on:
[type-envgen] Invalid environment variables:
DATABASE_URL: missing
JWT_SECRET: must be at least 32 characters
NODE_ENV: must be one of: development, production, test
PORT: must be at most 65535
The error messages never print the actual values, so secrets don't end up in your logs.
Other things it does:
- No tags needed: untagged keys are typed from their value (numbers, booleans, URLs, emails, UUIDs, dates, IPs, even JSON objects).
- Lots of types: about 40
@types map directly to zod 4 formats (jwt, hash, iban, e164, cidr…), plus checks like @pattern, @startswith and @nonempty, and transforms like @trim and @tolowercase.
- Framework targets:
--target vite | nextjs | astro | sveltekit | nuxt reads from the right place for each framework. It also warns if a key is missing the public prefix, so you don't ship a secret to the browser.
- Safe to regenerate: your own code in
env.ts is kept, and only the generated parts are replaced.
- CI and dev:
--check exits 1 in CI if the file is stale, and --watch regenerates while you develop.
- Setup:
npx type-envgen init asks a few questions and adds the npm scripts for you.
GitHub: https://github.com/OlaleyeFisayo/type-envgen
npm: type-envgen
It's MIT-licensed. I'd really like feedback, especially on what tags or targets you'd want next.