# Framework-Specific Environment Patterns
> **Part of**: [env-manager](../SKILL.md)
> **Category**: infrastructure
> **Reading Level**: Intermediate
## Purpose
Complete patterns for framework-specific environment variable handling: Next.js, Express, Flask, Django, Vite, and Create React App.
## Framework Detection
### Auto-Detection Logic
```python
from pathlib import Path
from typing import Optional
def detect_framework(project_dir: Path) -> Optional[str]:
"""Auto-detect framework from project structure."""
detection_patterns = {
'nextjs': ['next.config.js', 'next.config.mjs', 'next.config.ts'],
'vite': ['vite.config.js', 'vite.config.ts'],
'react': ['react-scripts', 'craco.config.js'],
'express': ['express', 'app.js', 'server.js'],
'flask': ['app.py', 'wsgi.py', 'requirements.txt'],
'django': ['manage.py', 'settings.py'],
'fastapi': ['main.py', 'fastapi']
}
# Check package.json for JS frameworks
package_json = project_dir / 'package.json'
if package_json.exists():
import json
with open(package_json) as f:
data = json.load(f)
deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})}
if 'next' in deps:
return 'nextjs'
elif 'vite' in deps:
return 'vite'
elif 'react-scripts' in deps:
return 'react'
elif 'express' in deps:
return 'express'
# Check for Python frameworks
requirements = project_dir / 'requirements.txt'
if requirements.exists():
content = requirements.read_text().lower()
if 'flask' in content:
return 'flask'
elif 'django' in content:
return 'django'
elif 'fastapi' in content:
return 'fastapi'
# Check for specific files
for framework, patterns in detection_patterns.items():
for pattern in patterns:
if (project_dir / pattern).exists():
return framework
return 'generic'
```
## Next.js
### File Precedence
Next.js loads env files in this order (higher precedence first):
```
1. .env.$(NODE_ENV).local (e.g., .env.production.local)
2. .env.local (always, except in test)
3. .env.$(NODE_ENV) (e.g., .env.production)
4. .env
```
### Public vs Private Variables
**Client-Side (Public):**
```bash
# Prefix with NEXT_PUBLIC_ for browser access
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_ANALYTICS_ID=UA-123456789
NEXT_PUBLIC_SITE_NAME=My App
```
**Server-Side (Private):**
```bash
# No prefix - only available server-side
DATABASE_URL=postgres://localhost:5432/mydb
JWT_SECRET=super-secret-key-never-expose
API_SECRET_KEY=sk_live_abc123
```
### Validation Rules
```python
def validate_nextjs_env(env_file: Path) -> Dict:
"""Validate Next.js environment variables."""
errors = []
warnings = []
vars_dict = parse_env_file(env_file)
for key, value in vars_dict.items():
# Check for secrets in public vars
if key.startswith('NEXT_PUBLIC_'):
secret_indicators = ['secret', 'key', 'password', 'token', 'private']
if any(indicator in key.lower() for indicator in secret_indicators):
errors.append({
'key': key,
'error': 'SECURITY: Secret in NEXT_PUBLIC_ variable (exposed to browser)'
})
# Check for API endpoints without NEXT_PUBLIC_
if 'api' in key.lower() and 'url' in key.lower():
if not key.startswith('NEXT_PUBLIC_'):
warnings.append({
'key': key,
'warning': 'API URL without NEXT_PUBLIC_ prefix (not accessible client-side)'
})
return {'errors': errors, 'warnings': warnings}
```
### File Structure Example
```bash
# .env (committed - shared defaults)
NEXT_PUBLIC_APP_NAME=My App
DATABASE_URL=postgres://localhost:5432/dev
# .env.local (gitignored - local overrides)
DATABASE_URL=postgres://localhost:5432/mylocal
JWT_SECRET=dev-jwt-secret-123
# .env.production (committed - production defaults)
NEXT_PUBLIC_API_URL=https://api.example.com
# .env.production.local (gitignored - production secrets)
DATABASE_URL=postgres://prod.example.com:5432/prod
JWT_SECRET=prod-jwt-secret-xyz
```
## Express/Node.js
### Standard Variables
```bash
# Node environment
NODE_ENV=development # or production, test
# Server configuration
PORT=3000
HOST=localhost
# Database
DATABASE_URL=postgres://localhost:5432/mydb
# Security
JWT_SECRET=your-secret-key
SESSION_SECRET=session-secret
# External services
REDIS_URL=redis://localhost:6379
SMTP_HOST=smtp.example.com
SMTP_PORT=587
```
### Validation Rules
```python
NODE_STANDARD_VARS = {
'NODE_ENV': ['development', 'production', 'test'],
'PORT': lambda v: v.isdigit() and 1 <= int(v) <= 65535,
'DATABASE_URL': lambda v: v.startswith(('postgres://', 'mysql://', 'mongodb://'))
}
def validate_nodejs_env(env_file: Path) -> List[Dict]:
"""Validate Node.js environment variables."""
errors = []
vars_dict = parse_env_file(env_file)
for key, validator in NODE_STANDARD_VARS.items():
if key in vars_dict:
value = vars_dict[key]
if isinstance(validator, list):
if value not in validator:
errors.append({
'key': key,
'error': f'Invalid value "{value}", expected one of {validator}'
})
elif callable(validator):
if not validator(value):
errors.append({
'key': key,
'error': 'Value validation failed'
})
return errors
```
## Flask/Python
### Standard Variables
```bash
# Flask configuration
FLASK_APP=app.py
FLASK_ENV=development # or production
FLASK_DEBUG=1
# Database (SQLAlchemy)
DATABASE_URL=postgresql://localhost/mydb
SQLALCHEMY_DATABASE_URI=postgresql://localhost/mydb
# Security
SECRET_KEY=your-secret-key-here
WTF_CSRF_SECRET_KEY=csrf-secret
# External services
REDIS_URL=redis://localhost:6379
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
```
### Python-dotenv Loading
```python
# Load environment in Flask app
from dotenv import load_dotenv
import os
# Load .env file
load_dotenv()
# Access variables
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
app.config['DATABASE_URL'] = os.getenv('DATABASE_URL')
```
### Validation Rules
```python
FLASK_STANDARD_VARS = {
'FLASK_APP': lambda v: v.endswith('.py'),
'FLASK_ENV': ['development', 'production'],
'FLASK_DEBUG': ['0', '1', 'true', 'false']
}
def validate_flask_env(env_file: Path) -> List[Dict]:
"""Validate Flask environment variables."""
errors = []
vars_dict = parse_env_file(env_file)
# Check required Flask vars
required = ['FLASK_APP', 'SECRET_KEY']
for var in required:
if var not in vars_dict:
errors.append({
'key': var,
'error': f'Required Flask variable missing'
})
# Validate present vars
for key, validator in FLASK_STANDARD_VARS.items():
if key in vars_dict:
value = vars_dict[key]
if isinstance(validator, list) and value not in validator:
errors.append({
'key': key,
'error': f'Invalid value, expected one of {validator}'
})
elif callable(validator) and not validator(value):
errors.append({
'key': key,
'error': 'Value validation failed'
})
return errors
```
## Django
### Standard Variables
```bash
# Django configuration
DJANGO_SETTINGS_MODULE=myproject.settings
DJANGO_SECRET_KEY=your-secret-key
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
# Database
DATABASE_URL=postgres://localhost:5432/mydb
# Static/Media files
STATIC_ROOT=/var/www/static
MEDIA_ROOT=/var/www/media
# Security
CSRF_TRUSTED_ORIGINS=https://example.com
```
### Settings.py Integration
```python
# settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env
load_dotenv()
# Use env variables
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '').split(',')
```
## Vite
### Environment Variable Access
**Client-Side Variables:**
```bash
# Must prefix with VITE_ for access
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
VITE_ENABLE_ANALYTICS=true
```
**Usage in Code:**
```javascript
// Access via import.meta.env
const apiUrl = import.meta.env.VITE_API_URL;
const appTitle = import.meta.env.VITE_APP_TITLE;
```
### Validation Rules
```python
def validate_vite_env(env_file: Path) -> List[Dict]:
"""Validate Vite environment variables."""
errors = []
vars_dict = parse_env_file(env_file)
for key, value in vars_dict.items():
# Check for secrets in VITE_ vars
if key.startswith('VITE_'):
if any(s in key.lower() for s in ['secret', 'key', 'password', 'token']):
errors.append({
'key': key,
'error': 'SECURITY: Secret in VITE_ variable (exposed to browser)'
})
# Warn about non-VITE_ vars (won't be accessible)
elif not key in ['NODE_ENV', 'PORT']:
errors.append({
'key': key,
'warning': f'{key} not prefixed with VITE_ (not accessible in client code)'
})
return errors
```
## Create React App
### Environment Variable Access
**Client-Side Variables:**
```bash
# Must prefix with REACT_APP_ for access
REACT_APP_API_URL=https://api.example.com
REACT_APP_AUTH0_DOMAIN=example.auth0.com
REACT_APP_ENABLE_ANALYTICS=true
```
**Built-in Variables:**
```bash
NODE_ENV=development # Set by CRA automatically
PUBLIC_URL=/ # Public URL of the app
```
**Usage in Code:**
```javascript
// Access via process.env
const apiUrl = process.env.REACT_APP_API_URL;
const domain = process.env.REACT_APP_AUTH0_DOMAIN;
```
## Framework Comparison Table
| Next.js | NEXT_PUBLIC_ | All vars | Complex (4 files) | Dev only |
| Vite | VITE_ | All vars | .env.local > .env | Dev only |
| CRA | REACT_APP_ | All vars | .env.local > .env | Requires restart |
| Express | N/A | All vars | .env only | With nodemon |
| Flask | N/A | All vars | .env only | With debug mode |
| Django | N/A | All vars | .env only | With runserver |
## Summary
**Framework Detection**:
- Auto-detect from package.json, requirements.txt, or config files
- Support explicit --framework override
**Key Patterns**:
- ✅ Next.js: NEXT_PUBLIC_ for client, file precedence critical
- ✅ Express: Standard NODE_ENV, PORT, DATABASE_URL
- ✅ Flask: FLASK_APP, FLASK_ENV, SECRET_KEY required
- ✅ Django: DJANGO_SETTINGS_MODULE, DJANGO_SECRET_KEY
- ✅ Vite: VITE_ prefix for client access
- ✅ CRA: REACT_APP_ prefix for client access
**Security Rules**:
- Never put secrets in client-exposed vars (NEXT_PUBLIC_, VITE_, REACT_APP_)
- Validate format of framework-specific vars
- Check for required variables per framework
## Related References
- [Validation](validation.md): General validation workflows
- [Security](security.md): Secret protection patterns
- [Troubleshooting](troubleshooting.md): Framework-specific issues
---
**Lines**: 279 ✓ 200-280 range