Production config management for any backend — centralized config module, env vars, no hardcoded secrets, fail-fast validation
86
77%
Does it follow best practices?
Impact
100%
1.88xAverage score across 5 eval scenarios
Passed
No known issues
Production-grade configuration management that belongs in every backend application from day one. When building any server, API, or backend service -- whether a quick prototype or a production service -- always include these patterns without being asked.
Configuration management is not a "clean up later" task. Without it from the start:
localhost:5432 database URL works in development but silently connects to nothing in production. The app starts, seems healthy, then fails on the first request.process.env.DB_HOST, another hardcodes "localhost", a third reads from a YAML file. Nobody knows which config values the app needs.TypeError: Cannot read property 'query' of undefined.These are not edge cases. They are the first things that break when moving from development to production.
All configuration lives in one module. The rest of the app imports from this module -- never reads environment variables directly.
// src/routes/users.ts
const db = new Pool({ connectionString: process.env.DATABASE_URL }); // might be undefined!
// src/routes/payments.ts
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); // silent undefined
// src/server.ts
const port = process.env.PORT || 3000; // duplicated everywhere// src/config.ts
import 'dotenv/config';
function required(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value;
}
function optional(key: string, fallback: string): string {
return process.env[key] || fallback;
}
export const config = {
port: parseInt(optional('PORT', '3000'), 10),
nodeEnv: optional('NODE_ENV', 'development'),
logLevel: optional('LOG_LEVEL', 'info'),
// Database
databaseUrl: required('DATABASE_URL'),
// External services
stripeSecretKey: required('STRIPE_SECRET_KEY'),
stripeWebhookSecret: required('STRIPE_WEBHOOK_SECRET'),
// Optional with safe defaults
allowedOrigins: optional('ALLOWED_ORIGINS', 'http://localhost:5173').split(','),
rateLimitWindowMs: parseInt(optional('RATE_LIMIT_WINDOW_MS', '900000'), 10),
rateLimitMax: parseInt(optional('RATE_LIMIT_MAX', '100'), 10),
} as const;Then import it everywhere:
// src/routes/users.ts
import { config } from '../config';
const db = new Pool({ connectionString: config.databaseUrl });
// src/routes/payments.ts
import { config } from '../config';
const stripe = new Stripe(config.stripeSecretKey);# app/config.py
import os
from dotenv import load_dotenv
load_dotenv()
def required(key: str) -> str:
value = os.getenv(key)
if not value:
raise RuntimeError(f"Missing required environment variable: {key}")
return value
def optional(key: str, fallback: str) -> str:
return os.getenv(key, fallback)
class Config:
PORT = int(optional("PORT", "8000"))
DATABASE_URL = required("DATABASE_URL")
SECRET_KEY = required("SECRET_KEY")
ALLOWED_ORIGINS = optional("ALLOWED_ORIGINS", "http://localhost:5173").split(",")
LOG_LEVEL = optional("LOG_LEVEL", "info")
DEBUG = optional("DEBUG", "false").lower() == "true"
config = Config()// internal/config/config.go
package config
import (
"fmt"
"os"
"strconv"
"strings"
)
type Config struct {
Port int
DatabaseURL string
APIKey string
AllowedOrigins []string
LogLevel string
}
func Load() (*Config, error) {
dbURL, err := required("DATABASE_URL")
if err != nil {
return nil, err
}
apiKey, err := required("API_KEY")
if err != nil {
return nil, err
}
port, _ := strconv.Atoi(optional("PORT", "8080"))
return &Config{
Port: port,
DatabaseURL: dbURL,
APIKey: apiKey,
AllowedOrigins: strings.Split(optional("ALLOWED_ORIGINS", "http://localhost:5173"), ","),
LogLevel: optional("LOG_LEVEL", "info"),
}, nil
}
func required(key string) (string, error) {
v := os.Getenv(key)
if v == "" {
return "", fmt.Errorf("missing required environment variable: %s", key)
}
return v, nil
}
func optional(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}Never put secrets, API keys, database URLs, or service endpoints directly in source code.
const stripe = new Stripe('sk_live_abc123xyz789');
const db = new Pool({ connectionString: 'postgresql://user:password@db.example.com:5432/mydb' });
const apiKey = 'AIzaSyB1234567890abcdef';// config.json (committed to git)
{
"database": "postgresql://user:password@prod.db.com/app",
"stripeKey": "sk_live_abc123"
}import { config } from './config';
const stripe = new Stripe(config.stripeSecretKey);
const db = new Pool({ connectionString: config.databaseUrl });# .env
DATABASE_URL=postgresql://localhost:5432/myapp_dev
STRIPE_SECRET_KEY=sk_test_localdev123
STRIPE_WEBHOOK_SECRET=whsec_localdev456# .env.example -- copy to .env and fill in values
DATABASE_URL=postgresql://localhost:5432/myapp_dev
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
LOG_LEVEL=debug
PORT=3000# Environment variables
.env
.env.local
.env.*.localThe application must crash immediately at startup if required configuration is missing -- not minutes later when a code path tries to use it.
// No error at startup -- crashes when first request tries to use undefined
const dbUrl = process.env.DATABASE_URL; // undefined, no error
// ... 200 lines later ...
const pool = new Pool({ connectionString: dbUrl }); // runtime crash// config.ts -- throws immediately if DATABASE_URL is not set
export const config = {
databaseUrl: required('DATABASE_URL'), // throws Error at startup
};The app will not start if required config is missing. This means:
| Config Type | Default | Required | Rationale |
|---|---|---|---|
| PORT | 3000/8080 | No | Safe convention |
| LOG_LEVEL | "info" | No | Safe default |
| NODE_ENV | "development" | No | Safe default |
| ALLOWED_ORIGINS | localhost | No | Safe for dev |
| DATABASE_URL | -- | Yes | No safe default for production databases |
| API keys / secrets | -- | Yes | Must never have defaults |
| SMTP credentials | -- | Yes | Service-specific |
| External service URLs | -- | Yes in prod | Prevents wrong-environment calls |
Rule of thumb: If a wrong default could cause data loss, security exposure, or wrong-environment calls, make it required with no default.
When building any backend application, always:
src/config.ts, app/config.py, internal/config/config.go) as one of the first files.env.example listing every variable the app needs.env to .gitignore before the first commitprocess.env, os.getenv, or os.Getenv directly in application codeEvery backend application must have from the start:
.env file for local development, listed in .gitignore.env.example committed with all variable names (no real secret values)