Environment Variables and Secrets Management in Node.js: The Complete Guide

4 minute read

When you build applications with Node.js, you’ll often deal with sensitive information like:

  • API keys
  • Database connection strings
  • Secret tokens
  • Third-party service credentials

Hardcoding these secrets directly into your application code is dangerous.
Instead, the industry standard is to use environment variables and proper secrets management.

In this guide, you’ll learn:

  • What environment variables are
  • How to load and manage them in Node.js
  • Best practices for secrets management
  • Popular tools for secret storage
  • How to handle secrets in different environments (dev, staging, production)

What Are Environment Variables?

Environment variables are key-value pairs stored outside of your source code.

They act like hidden settings that your application can access at runtime, without exposing sensitive information inside the codebase.

For example:

# Example .env file
DATABASE_URL=mongodb://username:password@db.example.com:27017/myapp
API_KEY=sk_test_abc123def456
NODE_ENV=production

Your Node.js app can read these environment variables using process.env:

console.log(process.env.API_KEY);

Why Should You Use Environment Variables?

Security: Sensitive data stays outside your source code.

Flexibility: Easily change configuration across different environments (dev, test, prod).

Portability: Same codebase, different settings per environment.

Compliance: Many security standards (like GDPR, PCI DSS) require secrets to be properly managed.


Setting Up Environment Variables in Node.js

There are several ways to load environment variables in Node.js.

1. Manually Set Environment Variables

You can set environment variables when you start your Node app:

API_KEY=yourapikey node app.js

or on Windows:

set API_KEY=yourapikey && node app.js

2. Use a .env File with dotenv

The easier and more common approach is using a .env file and a library called dotenv.

Install it:

npm install dotenv

At the very top of your entry file (e.g., app.js):

require('dotenv').config();

Then create a .env file in your project root:

# .env
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=mySuperSecretKey

Now process.env.PORT, process.env.DATABASE_URL, and process.env.JWT_SECRET are available inside your app.


Example: Secure MongoDB Connection

Here’s a quick example of connecting to MongoDB using environment variables:

require('dotenv').config();
const mongoose = require('mongoose');

mongoose.connect(process.env.DATABASE_URL)
  .then(() => console.log('Connected to MongoDB!'))
  .catch(err => console.error('MongoDB connection error:', err));

You don’t expose the password anywhere in your code.
The sensitive URL stays hidden inside .env.


How to Organize and Manage Secrets

As your project grows, managing secrets becomes more complex.

You’ll need:

  • Different values per environment (local, staging, production)
  • Safe storage (no pushing .env files to GitHub)
  • Rotation (updating secrets without downtime)

Here’s how to do it properly:


1. Different .env Files per Environment

You can create separate files:

  • .env for local development
  • .env.staging for the staging server
  • .env.production for production

Load them conditionally:

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`
});

When starting your app:

NODE_ENV=production node app.js

This will load .env.production automatically.


2. Never Commit Secrets to Git

Always ignore .env files using .gitignore:

# .gitignore
.env
.env.*

And use tools like git-secrets to scan commits for secret leaks.


3. Use a Secrets Manager in Production

When you move to production, you should avoid .env files altogether.
Instead, use secrets management systems like:

  • AWS Secrets Manager
  • HashiCorp Vault
  • Doppler
  • Google Secret Manager
  • Azure Key Vault

Example: Fetching secrets from AWS Secrets Manager inside a Lambda function.

const AWS = require('aws-sdk');
const client = new AWS.SecretsManager();

async function getSecrets() {
  const secret = await client.getSecretValue({ SecretId: 'my/secret/id' }).promise();
  return JSON.parse(secret.SecretString);
}

Then you can pull live secrets at runtime securely.


Advanced Secrets Management Techniques

When you build large-scale Node.js apps, you’ll want to go even deeper.

a. Auto-Reload Secrets Without Restarting App

Some secret managers support dynamic secret rotation.
You can listen for secret updates and reload config without downtime.

Example:

  • Set up secret rotation in AWS Secrets Manager
  • Reload config inside your Node.js app when version changes

b. Encrypt Secrets Locally

If you must store a .env file locally, encrypt it:

  • Encrypt the file using a tool like sops.
  • Decrypt at runtime or during deployment.

This way even if your .env file leaks, it’s still useless without the decryption key.


c. Use Environment Variables for Secrets, Config Files for Non-Sensitive Data

Follow the Twelve-Factor App recommendation:

  • Keep secrets (passwords, API keys) in env vars.
  • Keep non-sensitive config (e.g., feature toggles, labels) in versioned config files like config.json.

Separation of concerns makes systems easier to maintain.


Example Project: Secure API with Environment Variables

Let’s put it all together.

Imagine you are building a simple Express API.

.env:

PORT=4000
JWT_SECRET=mysecret123

server.js:

require('dotenv').config();
const express = require('express');
const jwt = require('jsonwebtoken');

const app = express();
const PORT = process.env.PORT || 3000;

app.get('/token', (req, res) => {
  const token = jwt.sign({ user: 'admin' }, process.env.JWT_SECRET, { expiresIn: '1h' });
  res.json({ token });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Securely generate tokens without exposing the secret key anywhere in the codebase.


Best Practices Summary

Best Practice Why It Matters
Never hardcode secrets in source code Prevent accidental leaks
Always use .gitignore for .env Avoid committing secrets
Use a real secrets manager for production More secure, scalable
Separate secrets from app config Cleaner architecture
Rotate secrets regularly Limit damage if leaks happen
Limit permissions (Principle of Least Privilege) Minimize attack surface

Here’s a cheat sheet of tools you should know:

Tool Purpose
dotenv Load .env files
dotenv-safe Ensure required env vars exist
vault CLI Interact with HashiCorp Vault
aws-sdk Fetch secrets from AWS Secrets Manager
dotenv-expand Expand nested env variables
sops Encrypt/decrypt .env files

Conclusion

Proper environment variables and secrets management is non-negotiable when building Node.js applications.

At first, using .env files and dotenv will be enough.
But as you scale into production environments, you’ll want to move to cloud-native secrets managers for maximum security.

Remember:

  • Protect secrets like you protect passwords.
  • Automate rotation wherever possible.
  • Follow best practices from day one, so you don’t have painful migrations later.

With the right approach, you’ll build secure, scalable, and professional Node.js applications that handle secrets safely.


Leave a comment