❌ Organizing env vars just before deployment
→ Missing variables, naming inconsistency, deployment delays
✅ Establish convention at design stage
→ Consistent naming, clear categorization, fast deployment
Environment Variable Naming Rules
Prefix
Purpose
Exposure Scope
Example
NEXT_PUBLIC_
Client-exposed
Browser
NEXT_PUBLIC_API_URL
DB_
Database
Server only
DB_HOST, DB_PASSWORD
API_
External API keys
Server only
API_STRIPE_SECRET
AUTH_
Authentication
Server only
AUTH_SECRET, AUTH_GOOGLE_ID
SMTP_
Email service
Server only
SMTP_HOST, SMTP_PASSWORD
STORAGE_
File storage
Server only
STORAGE_S3_BUCKET
⚠️ Security Principles
- Never expose anything except NEXT_PUBLIC_* to client
- API keys and passwords must be server-only variables
- Never commit sensitive info in .env files
.env File Structure
Project Root/
├── .env.example # Template (in Git, values empty)
├── .env.local # Local development (Git ignored)
├── .env.development # Development env defaults
├── .env.staging # Staging env defaults
├── .env.production # Production defaults (no sensitive info)
└── .env.test # Test environment
.env.example Template
# .env.example - This file is included in Git# Set actual values in .env.local# ===== App Settings =====
NODE_ENV=development
NEXT_PUBLIC_APP_URL=http://localhost:3000
# ===== Database =====
DB_HOST=
DB_PORT=5432
DB_NAME=
DB_USER=
DB_PASSWORD=
# ===== Authentication =====
AUTH_SECRET= # openssl rand -base64 32
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# ===== External Services =====
NEXT_PUBLIC_API_URL=
API_STRIPE_SECRET=
SMTP_HOST=
SMTP_USER=
SMTP_PASSWORD=
Environment-wise Value Classification
Variable Type
.env.example
.env.local
CI/CD Secrets
App URL
Template
Local value
Per-env value
API endpoints
Template
Local/dev
Per-env value
DB password
Empty
Local value
✅ Secrets
API keys
Empty
Test key
✅ Secrets
JWT Secret
Empty
Local value
✅ Secrets
Environment Variable Validation
// lib/env.ts - Validate env vars at app startupimport { z } from'zod';
const envSchema = z.object({
// RequiredDATABASE_URL: z.string().url(),
AUTH_SECRET: z.string().min(32),
// Optional (with defaults)NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
// Client-exposedNEXT_PUBLIC_APP_URL: z.string().url(),
});
// Validation and type inferenceexportconst env = envSchema.parse(process.env);
// Type-safe usage// env.DATABASE_URL ← autocomplete supported
// ❌ Tied to specific typefunctioncalculateOrderTotal(order: Order) {
return order.items.reduce((sum, item) => sum + item.price, 0)
}
// ✅ Generalized with interfaceinterfaceHasPrice { price: number }
function calculateTotal<T extendsHasPrice>(items: T[]) {
return items.reduce((sum, item) => sum + item.price, 0)
}
// Can be used in various placescalculateTotal(order.items)
calculateTotal(cart.products)
calculateTotal(invoice.lineItems)
6.2 Component Design
Composable Components
// ❌ Hardcoded structurefunctionUserCard({ user }: { user: User }) {
return (
<divclassName="card"><imgsrc={user.avatar} /><h3>{user.name}</h3><p>{user.email}</p></div>
)
}
// ✅ ComposablefunctionCard({ children, className }: CardProps) {
return<divclassName={cn("card", className)}>{children}</div>
}
functionAvatar({ src, alt }: AvatarProps) {
return<imgsrc={src}alt={alt}className="avatar" />
}
// Use by combining
<Card>
<Avatarsrc={user.avatar}alt={user.name} /><h3>{user.name}</h3><p>{user.email}</p>
</Card>
Props Extensibility
// ❌ Limited propsinterfaceButtonProps {
label: stringonClick: () =>void
}
// ✅ Extend HTML attributesinterfaceButtonPropsextendsReact.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'outline' | 'ghost'size?: 'sm' | 'md' | 'lg'
}
// All button attributes available
<Buttontype="submit" disabled={isLoading}>
Save
</Button>
6.3 Extraction Criteria
When to Extract as Function
1. Same logic used 2+ times
2. Logic is complex enough to need a name
3. Logic that needs testing
4. Can be used in other files
When to Extract as Component
1. Same UI pattern repeats
2. Has independent state
3. Is a reusable unit
4. JSX over 50 lines