Node.js Security

How to Store API Keys
Securely in Node.js

The right way — from .env files in development to secrets managers in production.

8 min read·Updated Apr 2026

How to store API keys in Node.js securely is one of those things that looks simple but has several ways to go wrong — hardcoding in source files, committing .env files, using production keys in development, or deploying keys in a way that leaks them into logs. This guide covers every layer from local dev to production.

The Wrong Way (and Why Developers Do It)

❌ Never do this
// Hardcoded — worst option
const stripe = new Stripe("sk_live_abc123xyz");

// In a config file committed to Git
module.exports = {
  openaiKey: "sk-proj-abc123",
  awsSecret: "wJalrXUtnFEMI"
};

// Passed as a CLI argument
node app.js --api-key=sk-live-abc
✅ The right way
// Read from environment
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

// .env file (never committed)
// STRIPE_SECRET_KEY=sk_live_abc123xyz

// Validated on startup
if (!process.env.STRIPE_SECRET_KEY) {
  throw new Error("STRIPE_SECRET_KEY is required");
}

Step 1 — Install dotenv and Create Your .env File

npm install dotenv
# .env — never commit this file
STRIPE_SECRET_KEY=sk_live_your_key_here
OPENAI_API_KEY=sk-proj-your_key_here
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
SENDGRID_API_KEY=SG.abc123
NODE_ENV=development
// At the very top of your entry file (index.js, app.js, server.js)
require('dotenv').config();

// Or with ES modules
import 'dotenv/config';

// Now access keys via process.env
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

Load dotenv before anything else

require('dotenv').config() must run before any code that reads process.env. If you import a module that reads env vars at import time, dotenv must be called first — or use the -r dotenv/config flag: node -r dotenv/config app.js.

Step 2 — .gitignore Is Non-Negotiable

Add these lines to your .gitignore before the first git add:

# .gitignore
.env
.env.*
.env.local
.env.*.local
!.env.example   # Allow the example file to be committed

Commit a .env.example alongside it — this documents what variables are needed without exposing real values:

# .env.example — safe to commit, no real secrets
STRIPE_SECRET_KEY=sk_live_your_key_here
OPENAI_API_KEY=sk-proj-your_key_here
DATABASE_URL=postgresql://user:password@localhost:5432/myapp_dev
NODE_ENV=development

Step 3 — Validate on Startup

Fail fast if a required variable is missing rather than allowing a runtime error deep in your code when the key is first used:

// config.js — centralise all env var access
require('dotenv').config();

const required = [
  'STRIPE_SECRET_KEY',
  'OPENAI_API_KEY',
  'DATABASE_URL',
];

for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
}

module.exports = {
  stripe: {
    secretKey: process.env.STRIPE_SECRET_KEY,
  },
  openai: {
    apiKey: process.env.OPENAI_API_KEY,
  },
  db: {
    url: process.env.DATABASE_URL,
  },
};

Import config.js at application startup. If any variable is missing, the process exits with a clear error message rather than a cryptic undefined error later.

Multiple Environments

Use separate .env files per environment and load the right one based on NODE_ENV:

# File structure
.env.development   # local dev keys (test/sandbox keys only)
.env.test          # keys for CI/CD test runs
.env.production    # never stored in the repo — set on the server

# All three in .gitignore
.env.*
// Load the right file
require('dotenv').config({
  path: `.env.${process.env.NODE_ENV || 'development'}`
});

Never use production keys in development

Create sandbox or test accounts for every API you use. Most providers (Stripe, OpenAI, Twilio, SendGrid) have separate test/sandbox environments with separate keys. If you develop with production keys, a bug in development can cause real charges, real emails to customers, or real data access.

Production — Don't Deploy a .env File

A .env file is a development convenience. In production, set environment variables directly on your platform — they're injected at runtime and never touch your codebase or Git history:

Vercel
Project Settings → Environment Variables → Add variable per environment
Railway
Project → Variables → New Variable
Heroku
Settings → Config Vars → Reveal Config Vars
Render
Service → Environment → Add Environment Variable
Fly.io
fly secrets set STRIPE_KEY=sk_live_... (CLI)
AWS EC2
Systems Manager Parameter Store or Secrets Manager

Team Secrets Management

When your team grows beyond one developer, sharing .env files via Slack or email is a security problem. Someone will paste a key in the wrong channel, or an old key will live in message history forever. A secrets manager solves this properly:

Doppler
Syncs secrets across all environments. Developers run doppler run -- node app.js and never see or store the actual keys. Free for up to 5 users. Integrates with Vercel, Heroku, Railway.
npm install -g @dopplerhq/cli
doppler setup
doppler run -- node app.js
Infisical
Open-source secrets manager you can self-host. Full audit trails, role-based access, SDK for Node. No vendor lock-in.
npm install @infisical/sdk

import InfisicalClient from "@infisical/sdk";
const client = new InfisicalClient({ token });
const secret = await client.getSecret("STRIPE_KEY");

Never Log API Keys

The final leak vector: your own application logs. These are the most common ways keys end up in logs:

// ❌ Never log the full config object
console.log("App config:", config);          // Logs all keys
console.log("Request:", req.headers);        // May contain Authorization header
console.error("DB error:", connectionString); // May contain password in URL

// ✅ Log structured data without secrets
console.log("Stripe configured:", !!process.env.STRIPE_SECRET_KEY);
console.log("Request from:", req.ip, req.method, req.path);
// Never log req.headers or req.body in production

Before sharing any log output with a colleague, on StackOverflow or with an AI assistant, run it through a sanitizer to catch any keys that slipped through.

Strip Keys From Logs Before Sharing

Catches API keys, database URLs and tokens in logs. 100% client-side — nothing uploaded.

Open Log Sanitizer — Free →

FAQ

How do I store API keys in Node.js? +

Use dotenv to load a .env file in development, and access keys via process.env.YOUR_KEY_NAME. Add .env to .gitignore immediately. In production, set variables directly on your hosting platform.

Should I commit my .env file to Git? +

Never. Add .env to .gitignore before your first commit. If you accidentally commit it, revoke the exposed keys immediately — see How to Remove API Keys From Git History for the cleanup steps.

What is the difference between process.env and dotenv? +

process.env is Node's built-in object for reading environment variables set by the OS or platform. dotenv reads a .env file and injects its values into process.env. In production you don't need dotenv — the platform sets the variables directly.

How do I share API keys with my team without emailing them? +

Use a secrets manager. Doppler (free for small teams) syncs secrets across all environments and team members without anyone ever seeing the raw key values. Infisical is the open-source self-hosted alternative. Both integrate directly with Node.js via their CLIs and SDKs.

Related