Development vs Production Differences Β· Astro Tech Blog

The Core Distinction

Development and production environments serve fundamentally different goals:

ConcernDevelopmentProduction
GoalDeveloper productivityUser reliability
ErrorsVerbose stack tracesSafe, generic messages
LoggingHuman-readable, noisyStructured JSON, level-filtered
PerformanceUnoptimised, fast iterationOptimised, max throughput
RestartsManual (Ctrl+C, node --watch)Automatic (PM2, systemd)
Secrets.env file (gitignored)Environment variables
DependenciesFull devDependenciesnpm ci --production

1. The NODE_ENV Variable

NODE_ENV is the standard way to tell your app which environment it’s in:

const isProduction = process.env.NODE_ENV === 'production';

if (isProduction) {
  // Use production database
  // Enable caching
  // Disable debug logging
} else {
  // Use local development database
  // Pretty-print logs
  // Show error stack traces
}

Set it when starting your app:

# Development
NODE_ENV=development node app.js

# Production
NODE_ENV=production node app.js

Important: Many libraries (Express, React, Vue) change behaviour based on NODE_ENV. Express disables view caching and verbose logging in development mode. Always set NODE_ENV=production in production.

2. Error Handling Differences

Development β€” Show Everything

if (!isProduction) {
  app.use((err, req, res, next) => {
    console.error(err.stack); // Full stack trace
    res.status(500).json({
      error: err.message,
      stack: err.stack,           // Expose internals
      details: err.details,       // Debug info
    });
  });
}

Production β€” Hide Internals

if (isProduction) {
  app.use((err, req, res, next) => {
    // Log the full error internally
    console.error(JSON.stringify({
      timestamp: new Date().toISOString(),
      error: err.message,
      stack: err.stack,
      requestId: req.id,
      url: req.url,
    }));

    // Send a safe, generic message to the client
    res.status(500).json({
      error: 'Internal server error',
      requestId: req.id,       // For support reference
    });
  });
}

3. Logging Differences

Development β€” Pretty, Verbose

// Development logger β€” human-readable
const devLogger = {
  info(msg, meta) { console.log(`ℹ️ ${msg}`, meta || ''); },
  warn(msg, meta) { console.warn(`⚠️ ${msg}`, meta || ''); },
  error(msg, meta) { console.error(`❌ ${msg}`, meta || ''); },
  debug(msg, meta) { console.debug(`πŸ” ${msg}`, meta || ''); },
};

Production β€” Structured, Filtered

// Production logger β€” JSON, level-filtered
const prodLogger = {
  info(msg, meta) {
    if (process.env.LOG_LEVEL === 'debug') {
      console.log(JSON.stringify({ level: 'info', msg, ...meta, timestamp: new Date().toISOString() }));
    }
  },
  warn(msg, meta) {
    console.warn(JSON.stringify({ level: 'warn', msg, ...meta, timestamp: new Date().toISOString() }));
  },
  error(msg, meta) {
    console.error(JSON.stringify({ level: 'error', msg, ...meta, timestamp: new Date().toISOString() }));
  },
};

4. Dependency Management

# Development β€” install everything
npm install

# Production β€” only runtime dependencies
npm ci --production
# Or: NODE_ENV=production npm ci

devDependencies vs dependencies:

{
  "dependencies": {
    "express": "^4.18.0",
    "pg": "^8.11.0"
  },
  "devDependencies": {
    "jest": "^29.0.0",       // Testing β€” not needed in prod
    "eslint": "^8.0.0",      // Linting β€” not needed in prod
    "nodemon": "^3.0.0",     // Auto-restart β€” not needed in prod
    "supertest": "^6.0.0"    // Testing β€” not needed in prod
  }
}

5. Startup Differences

Development β€” Watch Mode

# Auto-restart on file changes
node --watch app.js

# Or with nodemon for older Node versions
npx nodemon app.js

Production β€” Process Manager

# PM2 β€” production process manager
pm2 start app.js -i max --name my-api
pm2 save
pm2 startup

6. Environment Variables

Development β€” .env File

# .env (gitignored)
PORT=3000
DATABASE_URL=postgres://localhost:5432/myapp_dev
DEBUG=true

Production β€” System Environment

# Set by deployment platform (Heroku, Railway, Docker, etc.)
# Never use .env files in production
export PORT=8080
export DATABASE_URL=postgres://user:pass@prod-host:5432/myapp
export NODE_ENV=production

7. Security Differences

FeatureDevelopmentProduction
CORSAllow all origins (*)Restrict to specific domains
Rate limitingDisabledEnabled (100 req/min per IP)
HTTPSSelf-signed certValid TLS certificate
Helmet headersOptionalEnabled (all security headers)
Session secretsHardcoded or simpleStrong random secret

8. Database Configuration

// config/database.js
const dbConfig = {
  development: {
    host: 'localhost',
    port: 5432,
    database: 'myapp_dev',
    user: 'dev_user',
    password: 'dev_password',
    max: 5,                // Small pool for dev
    idleTimeoutMillis: 30000,
  },
  production: {
    host: process.env.DB_HOST,
    port: parseInt(process.env.DB_PORT) || 5432,
    database: process.env.DB_NAME,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    max: 25,                // Larger pool for production
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 5000,
    ssl: { rejectUnauthorized: true },  // Enforce SSL
  },
};

module.exports = dbConfig[process.env.NODE_ENV || 'development'];

9. Graceful Shutdown

Production requires clean shutdown β€” finish in-flight requests, close connections:

function shutdown(signal) {
  console.log(`${signal} received. Shutting down gracefully...`);

  server.close(() => {
    console.log('HTTP server closed');
    db.pool.end(() => {
      console.log('DB pool closed');
      process.exit(0);
    });
  });

  // Force exit after timeout
  setTimeout(() => {
    console.error('Forced shutdown after 10s');
    process.exit(1);
  }, 10000);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));  // Production (PM2, Docker, K8s)
process.on('SIGINT', () => shutdown('SIGINT'));     // Development (Ctrl+C)

Quick Reference: Environment Checklist

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

module.exports = {
  isDev: env === 'development',
  isProd: env === 'production',
  isTest: env === 'test',

  // Safe errors in prod, verbose in dev
  errorResponse: (err) => ({
    error: env === 'production' ? 'Internal server error' : err.message,
    ...(env !== 'production' && { stack: err.stack }),
  }),

  // Pretty logs in dev, JSON in prod
  log: env === 'production'
    ? (level, msg, meta) => process.stdout.write(JSON.stringify({ level, msg, ...meta }) + '\n')
    : (level, msg, meta) => console[level](`[${level.toUpperCase()}] ${msg}`, meta || ''),
};

Key Takeaways

  • NODE_ENV=production changes behaviour in Express, React, and many libraries β€” always set it
  • Never leak stack traces in production β€” log them internally, send safe messages to clients
  • Use structured JSON logging in production β€” it’s parseable by log aggregators
  • Install only dependencies in production (npm ci --production)
  • Use .env files only in development β€” use system environment variables in production
  • Graceful shutdown (SIGTERM β†’ close server β†’ close DB β†’ exit) prevents data loss
  • Rate limiting, CORS restrictions, HTTPS, and security headers are production-only concerns
  • Connection pool sizes should be larger in production (more concurrent users)