How to Handle Environment Variables in a Full Stack Node.js and React App

If you’re building a modern full stack application with Node.js on the backend and React on the frontend, handling environment variables the right way is one of the most underrated skills you can master. Done correctly, it keeps your secrets safe, your deployments predictable, and your team productive. Done poorly, it can leak API keys to the public, break your staging environment, or expose your database credentials on GitHub.

In this hands-on tutorial, we will walk through exactly how to set up environment variables in a full stack app, covering development, staging, and production. You’ll get real code snippets, a clear folder structure, and a checklist to avoid the most common mistakes.

Why Environment Variables Matter in a Full Stack App

Environment variables are key-value pairs that live outside your codebase. They tell your app where to connect, which keys to use, and how to behave depending on the environment it’s running in.

  • Security: Keep API keys, database URLs, and JWT secrets out of your Git repository.
  • Portability: The same codebase can run locally, on staging, and in production without any code changes.
  • Team collaboration: Each developer can have their own local config without conflicts.
  • Compliance: Most security standards (SOC 2, ISO 27001) require secrets to be externalized.
code laptop developer

The Golden Rule: Backend vs Frontend Environment Variables

Here is the single most important concept to understand before writing a single line of code:

Anything you put in a frontend environment variable will end up in the client bundle. It is NOT a secret.

React, Vite, Next.js and similar tools inject env variables at build time. Once your JavaScript bundle is shipped to the browser, anyone can open DevTools and read those values. So the rule is simple:

Variable Type Backend (Node.js) Frontend (React/Vite)
Database password Yes Never
Stripe secret key Yes Never
Stripe publishable key Optional Yes
API base URL No Yes
JWT signing secret Yes Never

Recommended Folder Structure

For a typical full stack app with a separate backend and frontend, here is the structure we recommend:

my-app/
├── backend/
│   ├── .env.development
│   ├── .env.staging
│   ├── .env.production
│   ├── .env.example
│   └── src/
├── frontend/
│   ├── .env.development
│   ├── .env.staging
│   ├── .env.production
│   ├── .env.example
│   └── src/
├── .gitignore
└── package.json

Your .gitignore at the root should always contain:

.env
.env.local
.env.development
.env.staging
.env.production
!.env.example
code laptop developer

Setting Up the Backend (Node.js + Express)

1. Install dotenv

cd backend
npm install dotenv

2. Create your .env files

backend/.env.development

NODE_ENV=development
PORT=4000
DATABASE_URL=postgres://user:pass@localhost:5432/myapp_dev
JWT_SECRET=dev-only-not-secure
STRIPE_SECRET_KEY=sk_test_xxxxx
CORS_ORIGIN=http://localhost:5173

backend/.env.production (this file should never be committed, it lives only on the production server or in your secret manager)

NODE_ENV=production
PORT=4000
DATABASE_URL=postgres://prod_user:strong_password@db.internal:5432/myapp
JWT_SECRET=use-a-64-char-random-string-here
STRIPE_SECRET_KEY=sk_live_xxxxx
CORS_ORIGIN=https://adproductstogo.com

3. Load the right file at startup

backend/src/config.js

import dotenv from 'dotenv';
import path from 'path';

const env = process.env.NODE_ENV || 'development';
dotenv.config({ path: path.resolve(process.cwd(), `.env.${env}`) });

const required = ['DATABASE_URL', 'JWT_SECRET', 'STRIPE_SECRET_KEY'];
required.forEach((key) => {
  if (!process.env[key]) {
    throw new Error(`Missing required env variable: ${key}`);
  }
});

export const config = {
  env,
  port: parseInt(process.env.PORT, 10) || 4000,
  databaseUrl: process.env.DATABASE_URL,
  jwtSecret: process.env.JWT_SECRET,
  stripeSecret: process.env.STRIPE_SECRET_KEY,
  corsOrigin: process.env.CORS_ORIGIN,
};

Notice the validation step: the app crashes immediately if a critical variable is missing. This is far better than discovering the issue at 3 AM in production.

4. Run it with the right environment

"scripts": {
  "dev": "NODE_ENV=development node src/index.js",
  "staging": "NODE_ENV=staging node src/index.js",
  "start": "NODE_ENV=production node src/index.js"
}

Setting Up the Frontend (React with Vite)

Vite is the modern standard in 2026. It uses the VITE_ prefix to expose variables to the client. Any variable without this prefix stays private to the build process.

1. Create your .env files

frontend/.env.development

VITE_API_URL=http://localhost:4000/api
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
VITE_ENV=development

frontend/.env.production

VITE_API_URL=https://api.adproductstogo.com
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxx
VITE_ENV=production

2. Use them in your React code

// frontend/src/lib/api.js
const API_URL = import.meta.env.VITE_API_URL;

export async function fetchProducts() {
  const res = await fetch(`${API_URL}/products`);
  return res.json();
}

3. Build commands per environment

"scripts": {
  "dev": "vite",
  "build:staging": "vite build --mode staging",
  "build": "vite build --mode production"
}

Managing Secrets Safely in 2026

Storing secrets in plain .env files on a server is acceptable for small projects, but as you scale you should adopt a secret manager. Here are the most popular options:

  1. Doppler: Excellent developer experience, syncs to any environment with a single CLI command.
  2. AWS Secrets Manager: Best if you are already on AWS, integrates with IAM.
  3. HashiCorp Vault: The enterprise standard, more complex to set up.
  4. Infisical: Open source alternative with great Git integration.
  5. Vercel / Netlify environment variables: Built-in if you deploy on those platforms.

Whichever you choose, the principle stays the same: secrets should be injected at runtime, not baked into your image or repository.

code laptop developer

How to Avoid Leaking Variables to the Client Bundle

This is where many teams get burned. Here’s a defensive checklist:

  • Never use the VITE_, REACT_APP_, or NEXT_PUBLIC_ prefix for anything sensitive.
  • Audit your bundle: run grep -r "sk_live" dist/ after building, you should get zero results.
  • Use a server-side proxy when calling third party APIs that require secret keys.
  • Rotate any secret that has ever been committed to Git, even if you removed it later. Git history keeps everything.
  • Add a pre-commit hook with gitleaks or trufflehog to catch accidental commits.

The .env.example File: A Must-Have

Every project should have a committed .env.example that documents which variables are needed, without revealing actual values:

# backend/.env.example
NODE_ENV=
PORT=
DATABASE_URL=
JWT_SECRET=
STRIPE_SECRET_KEY=
CORS_ORIGIN=

New developers can copy it with cp .env.example .env.development and fill in their values. No more “hey, what env vars do I need again?” messages on Slack.

code laptop developer

Staging Environment: The Forgotten Hero

Many teams have only development and production. Adding a staging environment that mirrors production with non-sensitive test data is one of the best investments you can make:

  • Test new features with real-world configuration before going live.
  • Catch missing env variables before they hit production.
  • Allow QA and stakeholders to validate without risk.

Common Mistakes to Avoid

Mistake Consequence Fix
Committing .env files Leaked secrets on GitHub Use .gitignore, rotate secrets
Using VITE_ prefix for secrets Secrets in client bundle Move to backend, use proxy
No validation at startup Silent failures in production Throw on missing required vars
Same secrets across environments Dev leak affects production Use distinct values per env
No .env.example Slow onboarding Document all required keys

FAQ

Can I use a single .env file for both backend and frontend?

Technically yes, but we strongly recommend against it. Frontend tools inject variables into the public bundle, so mixing backend secrets with frontend config dramatically increases the risk of leaks. Keep them in separate folders with separate files.

What is the difference between process.env and import.meta.env?

process.env is the Node.js global used on the backend. import.meta.env is what Vite exposes in the browser. Webpack-based React apps (like Create React App, now legacy) used process.env.REACT_APP_*.

Are environment variables encrypted?

No. They are plain text in memory and on disk. If you need encryption at rest, use a secret manager like AWS Secrets Manager, Doppler, or Infisical.

What if I accidentally pushed a .env file to GitHub?

Rotate every secret immediately. Removing the file or rewriting history is not enough because bots scan public repositories within seconds. Then add the file to .gitignore and consider using a tool like gitleaks to prevent it from happening again.

Should I use dotenv in production?

It works, but most modern hosting platforms (Vercel, Render, Railway, Fly.io, AWS ECS) inject environment variables directly into the process. In those cases, you don’t need dotenv in production, only in local development.

How do I handle environment variables with Docker?

Pass them via docker run -e KEY=value, a --env-file flag, or in your docker-compose.yml using the environment or env_file section. Never bake secrets into your Docker image with ENV instructions.

Wrapping Up

Managing environment variables in a full stack app is not glamorous work, but it’s the foundation of a secure, maintainable codebase. By separating backend and frontend configurations, validating required variables at startup, using a proper .env.example, and adopting a secret manager as you scale, you’ll save your team countless hours and protect your business from preventable security incidents.

Start simple, automate as you grow, and never trust the client bundle with anything you wouldn’t post on Twitter.