Dotenv is dead
Today I learned from Liran Tal
that Node.js 20.6.0 brings support for loading environment variable files, just like the popular dotenv
package does:
node --env-file=.env main.mjs
You can even load multiple files (since Node.js 20.7.0):
node --env-file=.env --env-file=.env.local main.mjs
This post could have ended here, but I’ll take this opportunity to pass the overtly click-baity title and talk about how I manage environment variables in my Node.js projects.
There are several issues with reading directly from process.env
:
- It’s not type safe
- It’s not validated
- It’s not immutable
Type safety & validation
I like to use Zod to parse and validate outside data in my applications, and environment variables fall perfectly in that category.
I define a schema for the expected configuration, and run in on process.env
:
import { z } from 'zod'
const envSchema = z.object({
// See https://cjihrig.com/node_env_considered_harmful
NODE_ENV: z.enum(['development', 'production', 'test']).default('production'),
// External resource URIs
POSTGRESQL_URL: z.string().url(),
REDIS_URL: z.string().url(),
// Secrets
API_KEY: z.string().regex(/^[\da-f]{64}$/i),
// Booleans
DEBUG: z
.string()
.transform(value =>
['true', 'yes', '1', 'on'].includes(value.toLowerCase())
)
.default('false')
})
export const env = envSchema.parse(process.env)
More examples here.
We can now import this env
object anywhere in our application.
Better error messages
When some environment variables are missing or invalid, Zod will throw an error on parsing which may not look very good for humans:
We can easily fix this by obtaining the list of errors and formatting it nicely:
// ...
const parsed = envSchema.safeParse(process.env)
if (!parsed.success) {
console.error(
`Missing or invalid environment variable${
parsed.error.errors.length > 1 ? 's' : ''
}:
${parsed.error.errors
.map(error => ` ${error.path}: ${error.message}`)
.join('\n')}
`
)
process.exit(1)
}
export const env = parsed.data
Now that’s better:
Missing or invalid environment variables:
NODE_ENV: Required
Immutability
One issue with process.env
is that it’s mutable, and so is the env
object we just created. Let’s fix this by freezing it: 🥶
export const env = Object.freeze(parsed.data)
Security
If some of the environment variables are secrets, we can go further and delete
them from the process.env
global object, so they are only accessible from our
parsed env
object:
const secretEnvs: Array<keyof typeof envSchema.shape> = [
'POSTGRESQL_URL',
'REDIS_URL',
'SIGNATURE_PRIVATE_KEY'
]
for (const secretEnv of secretEnvs) {
delete process.env[secretEnv]
}
Freelance developer & founder