Migrating from dotenv to CtroEnv

2026-06-18·Ctrotech·
guidemigration

Migrating from dotenv to CtroEnv

dotenv loads files into process.env. CtroEnv validates, types, and secures them. Here's how to migrate incrementally.

Before: dotenv

A typical dotenv setup reads a .env file, then every file reads what it needs from process.env:

// config.ts
import "dotenv/config"

export const config = {
  port: parseInt(process.env.PORT ?? "3000", 10),
  dbUrl: process.env.DATABASE_URL!,
  jwtSecret: process.env.JWT_SECRET!,
  nodeEnv: process.env.NODE_ENV ?? "development",
}

Every access has a ! assertion or a manual default. Types are all string. A missing DATABASE_URL crashes at the point of use, not at startup.

Step 1: Install and Define a Schema

npm uninstall dotenv
npm install @ctroenv/core
// env.ts
import { defineEnv, string, number, pick } from "@ctroenv/core"

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

Step 2: Replace References

// Before
import { config } from "./config"
const port = config.port
const dbUrl = config.dbUrl

// After
import { env } from "./env"
env.PORT       // number — 3000
env.DATABASE_URL // string — guaranteed present

CtroEnv validates everything at import time. If DATABASE_URL is missing, you get a clear error:

Missing required environment variable: DATABASE_URL

No more mysterious Cannot read properties of undefined at 3 AM.

Step 3: Handle the .env File

Replace dotenv/config with @ctroenv/node:

npm install @ctroenv/node
import { defineEnv } from "@ctroenv/core"
import { loadEnv } from "@ctroenv/node"

export const env = defineEnv(schema, { source: loadEnv() })

loadEnv() reads .env → .env.development → .env.local with the same priority logic as dotenv. Pass { path: "../.." } for monorepo root files.

Step 4: Remove Type Assertions

dotenv leaves everything as string | undefined. CtroEnv gives you exact types:

BeforeAfter
process.env.PORT! → stringenv.PORT → number
parseInt(process.env.PORT ?? "3000")env.PORT → 3000
process.env.NODE_ENV as "dev" | "prod"env.NODE_ENV → "dev" | "prod"

Step 5: Add CI Validation (Optional)

npm install @ctroenv/cli
# .github/workflows/env-check.yml
- run: npx ctroenv check --source .env

This compares your .env against the schema and exits 1 if any keys are missing.

Migration Checklist

  • Remove all process.env.X calls in your app
  • Replace dotenv/config import with @ctroenv/node setup
  • Add .secret() to sensitive vars
  • Add ctroenv check to CI
  • Remove process.env type extensions (you won't need them)

What You Gain

  • Startup validation — errors surface immediately
  • Full TypeScript types — no string | undefined
  • Secret masking — .secret() prevents accidental leaks
  • Runtime adapters — same schema for Node, Vite, Next.js