Course:Internet & Tools/
Lesson

When you ask ChatGPT to "set up a database connection," it gives you code that points to localhost. When you deploy that code to Vercel or Cloudflare, it crashes because there is no localhost on a cloud server. AI tools have zero context about your deployment target, they write for the happy path on your laptop and nothing else. Structuring your configuration per-environment is how you prevent those deployment surprises.

The three environments you will encounter

Most projects run through at least three stages before code reaches real users:

EnvironmentPurposeTypical settings
DevelopmentYour local machine, fast feedback, verbose logging, disposable dataDEBUG=true, LOG_LEVEL=debug, localhost URLs
StagingMirrors production infrastructure, final testing before launchDEBUG=false, LOG_LEVEL=info, staging URLs
ProductionLive traffic, real data, real consequencesDEBUG=false, LOG_LEVEL=warn, production URLs
# Development .env
NODE_ENV=development
DEBUG=true
LOG_LEVEL=debug
DATABASE_URL=postgresql://localhost:5432/devdb
API_URL=http://localhost:3001

# Production (set in Vercel/Cloudflare dashboard, not in a file)
NODE_ENV=production
DEBUG=false
LOG_LEVEL=warn
DATABASE_URL=postgresql://prod-db.internal:5432/production
API_URL=https://api.myapp.com
Copilot frequently autocompletes DATABASE_URL=postgresql://localhost:5432/mydb in config files meant for production. Always verify that production config is set through your hosting platform's dashboard, not hardcoded in any file that gets deployed.
02

Reading NODE_ENV in your code

NODE_ENV is the universally recognized variable for environment detection. Libraries like Express, React, and Next.js use it internally, and you should too:

const env = process.env.NODE_ENV || 'development';
const isDev = env === 'development';
const isProd = env === 'production';

if (isDev) {
  console.log('[DEBUG] Request body:', JSON.stringify(req.body));
}

// AI mistake: logging sensitive data in all environments
// ChatGPT often generates this:
console.log('Database URL:', process.env.DATABASE_URL); // Leaks in prod logs!

// Correct: only log in development
if (isDev) {
  console.log('Database URL:', process.env.DATABASE_URL);
}
03

The config moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. pattern

Scattering process.env reads throughout your codebase makes them hard to audit and easy to forget. A single config.js file that owns all environment logic is far better:

// config.js - single source of truth for all configuration
require('dotenv').config();

const env = process.env.NODE_ENV || 'development';

const configs = {
  development: {
    port: 3000,
    database: process.env.DATABASE_URL || 'postgresql://localhost:5432/devdb',
    logLevel: 'debug',
    isDev: true,
    corsOrigin: 'http://localhost:4321',
  },
  staging: {
    port: Number(process.env.PORT) || 8080,
    database: process.env.DATABASE_URL,
    logLevel: 'info',
    isDev: false,
    corsOrigin: 'https://staging.myapp.com',
  },
  production: {
    port: Number(process.env.PORT) || 8080,
    database: process.env.DATABASE_URL,
    logLevel: 'warn',
    isDev: false,
    corsOrigin: 'https://myapp.com',
  },
};

module.exports = configs[env] || configs.development;

Now the restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources. of your code imports clean config values instead of raw process.env strings:

const config = require('./config');

app.listen(config.port, () => {
  console.log(`Running on port ${config.port} [${process.env.NODE_ENV}]`);
});
04

Feature flags

Feature flags let you merge code that is not fully ready, then turn it on later without a new deployment:

# .env
FEATURE_NEW_DASHBOARD=true
FEATURE_BETA_API=false
FEATURE_DARK_MODE=true
// Boolean check - remember, process.env values are strings
if (process.env.FEATURE_NEW_DASHBOARD === 'true') {
  renderNewDashboard();
} else {
  renderOldDashboard();
}
Use caseHow flags help
Gradual rolloutEnable for 10% of users, monitor, then expand
A/B testingShow two variants and measure which performs better
Instant rollbackSet flag to false, no redeploy needed
Internal-only featuresEnable in staging, disable in production
Kill switchDisable a broken feature instantly
Feature flags add real complexity over time. Delete a flag as soon as the feature is fully rolled out. A codebase full of stale flag checks is a maintenance nightmare, and AI tools like Copilot will happily generate code that references flags you removed months ago.
05

Config validation at startup

The most frustrating production bugs are caused by a missing env var. The app starts fine, then explodes 30 seconds later when it tries to use undefined as a database URL. Fail fast instead:

// validate-env.js - run this before anything else
function validateEnv(requiredKeys) {
  const missing = requiredKeys.filter(key => !process.env[key]);

  if (missing.length > 0) {
    console.error('Missing required environment variables:');
    missing.forEach(key => console.error(`  - ${key}`));
    console.error('\nCheck your .env file or platform environment settings.');
    process.exit(1);
  }
}

// In production, these MUST be set
if (process.env.NODE_ENV === 'production') {
  validateEnv(['DATABASE_URL', 'JWT_SECRET', 'STRIPE_SECRET_KEY']);
}
06

Deploying to cloud platforms

Every major platform has its own way to set production environment variables. You never commitWhat is commit?A permanent snapshot of your staged changes saved in Git's history, identified by a unique hash and accompanied by a message describing what changed. production secrets, you set them in the platform's dashboard and they are injected at runtimeWhat is runtime?The environment that runs your code after it's written. Some languages need a runtime installed on the machine; others (like Go) bake it into the binary..

# Vercel - add via CLI or dashboard
vercel env add DATABASE_URL production
# Prompts you to enter the value securely

# Cloudflare Workers - secrets are encrypted at rest
wrangler secret put STRIPE_SECRET_KEY
# Value never appears in shell history

# Railway - set in the dashboard Variables tab
# Railway auto-injects DATABASE_URL for provisioned databases

# Docker - pass at runtime
docker run -e NODE_ENV=production -e DATABASE_URL=postgres://... myapp
PlatformHow to set varsWhere they live
VercelDashboard or vercel env addEncrypted, per-environment
Cloudflare Workerswrangler secret put or dashboardEncrypted secrets store
RailwayDashboard Variables tabAuto-injected at runtime
RenderDashboard Environment tabEncrypted, per-service
Docker-e flag or --env-filePassed at container start
07

Quick reference

ConceptBest practice
Environment detectionprocess.env.NODE_ENV \|\| 'development'
Config structureSingle config.js module, not scattered process.env reads
Feature flagsString comparison: === 'true', delete when fully rolled out
Startup validationCheck required vars exist, process.exit(1) if missing
Platform secretsSet in dashboard or CLI, never in committed files
AI-generated configAlways verify URLs and credentials are not hardcoded for localhost