Stop Hardcoding Secrets: How to Load Node.js Env Vars Securely
Published September 27, 2026 · Editorial policy

Hardcoding your database password or Stripe API key into a db.js file works fine on your local machine until you accidentally push it to GitHub and wake up to a massive cloud bill. Every developer shipping Node.js apps hits this wall, usually right before deployment when they realize their environment-specific settings are completely tangled up in their source code. By the end of this guide, you will have a rock-solid configuration pattern using native Node.js features to load, parse, and validate your environment variables securely without leaking secrets.
The Modern Native Approach: No Extra Dependencies Needed
If you are running Node.js version 20.6.0 or higher, you no longer need dotenv or any other external package to load a .env file into process.env. Node.js shipping with built-in .env support changes how we bootstrap applications.
Setting Up Your Local Environment
Create a file named .env in the absolute root directory of your project—not inside your src/ folder. Populate it with your development secrets and configuration keys using standard KEY=VALUE formatting without quotes or semicolons unless they are part of the actual value.
PORT=3000
DATABASE_URL=postgresql://postgres:secretpassword@localhost:5432/myapp_dev
STRIPE_SECRET_KEY=sk_test_51Mz...
NODE_ENV=development
Never commit this file to version control. Add .env to your .gitignore file immediately.
Running Node with the --env-file Flag
Instead of requiring a package at the top of your entry file, pass the --env-file flag directly to the Node.js binary when starting your application.
Open your terminal and run your entry point:
node --env-file=.env src/index.js
To verify it works, create a temporary src/index.js file to log the variables:
// src/index.js
console.log("Server running on port:", process.env.PORT);
console.log("Database target:", process.env.DATABASE_URL);
Run the command again. Your terminal output should match your configuration:
Server running on port: 3000
Database target: postgresql://postgres:secretpassword@localhost:5432/myapp_dev
If you are using package scripts for local development, update your package.json to include the flag so you don't have to type it manually every time:
{
"scripts": {
"start": "node --env-file=.env src/index.js",
"dev": "node --env-file=.env --watch src/index.js"
}
}
Now you can just run npm run dev and get hot-reloading with your environment variables loaded automatically.
Centralizing and Validating Your Config
Relying on process.env.VARIABLE_NAME scattered across fifty different files is a recipe for runtime crashes. If a key is missing, your app might run fine for hours until a specific user triggers a code path that accesses process.env.STRIPE_KEY, throwing an undefined error deep in your payment controller.
Create a dedicated configuration module to catch these errors at startup.
Writing the Config Wrapper
Create a file called src/config.js. This file will read process.env, validate that all required values exist, and export a frozen configuration object to the rest of your application.
// src/config.js
// A helper function to throw an error if a required env var is missing
function getRequiredEnv(key) {
const value = process.env[key];
if (!value) {
throw new Error(`Configuration Error: Missing required environment variable "${key}"`);
}
return value;
}
export const config = Object.freeze({
port: process.env.PORT || 3000,
env: process.env.NODE_ENV || 'development',
dbUrl: getRequiredEnv('DATABASE_URL'),
stripeKey: getRequiredEnv('STRIPE_SECRET_KEY'),
});
Now, import this config object anywhere you need an environment variable.
// src/server.js
import { config } from './config.js';
import express from 'express';
const app = express();
app.listen(config.port, () => {
console.log(`Server listening on port ${config.port} in ${config.env} mode`);
});
If you start your application without defining DATABASE_URL in your environment or .env file, Node.js will crash immediately with a clear, descriptive error message rather than failing silently halfway through execution.
Handling Multiple Environments Safely
Production environments rarely use .env files. On platforms like AWS ECS, Google Cloud Run, Kubernetes, or Render, you inject environment variables directly into the container or host runtime. Your application code must remain agnostic to how the variables get into process.env.
Production Startup Patterns
In production, avoid pointing Node to a .env file unless you are using an explicit orchestration tool designed for it. Instead, inject them via your deployment pipeline or systemd service file.
For a standard Linux server running via PM2, configure your ecosystem.config.cjs file to pass environment variables directly:
// ecosystem.config.cjs
module.exports = {
apps: [
{
name: 'myapp-prod',
script: './src/index.js',
instances: 'max',
exec_mode: 'cluster',
env_production: {
NODE_ENV: 'production',
PORT: 8080,
// In a real setup, pull these from secure CI/CD secrets manager,
// not plain text in git repos
DATABASE_URL: process.env.PROD_DATABASE_URL,
STRIPE_SECRET_KEY: process.env.PROD_STRIPE_SECRET_KEY,
},
},
],
};
When deploying, trigger PM2 with the production environment flag:
pm2 start ecosystem.config.cjs --env production
Common Failure Points and Troubleshooting
Even with native support, environment variables can trip you up. If your setup isn't behaving, check these common failure points first.
1. Variables Return undefined Despite Being in .env
- The Cause: You are using a Node.js version older than
20.6.0, or you forgot to pass the--env-fileflag when executing the node command. - The Fix: Run
node -vin your terminal. If it is below20.6.0, either upgrade your Node runtime or installdotenvand callimport 'dotenv/config'as the absolute first line of your entry file. If your version is correct, verify your npm script includes--env-file=.env.
2. Syntax Errors Caused by Quotation Marks or Spaces
- The Cause: Unlike bash, Node's native
.envparser treats quotes literally in some contexts or fails when spaces surround the=sign. - The Fix: Format your
.envfile strictly without spaces around the equals sign:
If your secret contains spaces or special characters, wrap the entire value in double quotes:# CORRECT API_KEY=abcdef123456 # INCORRECT (will include spaces or fail) API_KEY = abcdef123456JWT_SECRET="super secret phrase with spaces"
3. Accidentally Exposing Secrets to the Client-Side
- The Cause: You imported your backend
config.jsorprocess.envvariables into frontend code bundled with Vite, Webpack, or Next.js. - The Fix: Never use backend environment variables in frontend code. If you are using a bundler like Vite, only variables prefixed with
VITE_are exposed to the browser. Keep your server-side database URLs and private keys completely isolated from frontend bundles.
Moving Beyond Plain Text Files
As your infrastructure scales past a single server, managing .env files across a team becomes a security risk. Plain text files sent over Slack or stored in shared drives invariably leak.
Transition your workflow to a dedicated secrets manager as you grow. Tools like Infisical, Doppler, or cloud-native solutions like AWS Secrets Manager and Google Secret Manager allow you to fetch secrets programmatically at runtime.
You can inject these tools directly into your startup command. For example, using the Doppler CLI in production:
doppler run -- node src/index.js
This injects secrets directly into the Node.js process memory without ever writing a .env file to disk. By combining native Node.js environment loading, strict startup validation, and a centralized config module, your secrets stay secure, your app fails fast when misconfigured, and your codebase remains clean and portable across every environment you deploy to.


