# PATRON DE CONFIGURACION **Version:** 1.0.0 **Fecha:** 2025-12-08 **Prioridad:** RECOMENDADA - Seguir para consistencia **Sistema:** SIMCO + CAPVED --- ## PROPOSITO Definir patrones estandarizados para manejo de configuracion en todas las capas, incluyendo variables de entorno, archivos de configuracion, y validacion de settings. --- ## 1. PRINCIPIOS FUNDAMENTALES ``` ╔══════════════════════════════════════════════════════════════════════╗ ║ PRINCIPIOS DE CONFIGURACION ║ ╠══════════════════════════════════════════════════════════════════════╣ ║ ║ ║ 1. NUNCA hardcodear valores sensibles ║ ║ 2. SIEMPRE validar configuracion al iniciar ║ ║ 3. FALLAR RAPIDO si configuracion es invalida ║ ║ 4. SEPARAR configuracion por ambiente ║ ║ 5. CENTRALIZAR acceso a configuracion ║ ║ 6. DOCUMENTAR cada variable requerida ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════╝ ``` --- ## 2. ESTRUCTURA DE ARCHIVOS ### Estructura Recomendada ``` project/ ├── .env.example # Template con TODAS las variables (sin valores reales) ├── .env # Valores locales (NUNCA en git) ├── .env.development # Override para desarrollo ├── .env.staging # Override para staging ├── .env.production # Override para produccion (solo en servidor) │ ├── apps/backend/ │ └── src/ │ └── shared/ │ └── config/ │ ├── configuration.ts # Configuracion principal │ ├── config.validation.ts # Schema de validacion │ ├── database.config.ts # Config de BD │ ├── jwt.config.ts # Config de JWT │ └── index.ts # Export centralizado │ └── apps/frontend/ └── src/ └── shared/ └── config/ ├── env.ts # Variables de entorno └── constants.ts # Constantes de app ``` ### .env.example (Template) ```bash # ═══════════════════════════════════════════════════════════════════ # CONFIGURACION DE BASE DE DATOS # ═══════════════════════════════════════════════════════════════════ DB_HOST=localhost DB_PORT=5432 DB_NAME=myapp_development DB_USER=postgres DB_PASSWORD= # REQUERIDO: Password de BD DB_SSL=false # ═══════════════════════════════════════════════════════════════════ # CONFIGURACION DE JWT/AUTH # ═══════════════════════════════════════════════════════════════════ JWT_SECRET= # REQUERIDO: Minimo 32 caracteres JWT_EXPIRATION=1d JWT_REFRESH_EXPIRATION=7d # ═══════════════════════════════════════════════════════════════════ # CONFIGURACION DE SERVIDOR # ═══════════════════════════════════════════════════════════════════ NODE_ENV=development PORT=3000 API_PREFIX=api CORS_ORIGINS=http://localhost:5173 # ═══════════════════════════════════════════════════════════════════ # CONFIGURACION DE SERVICIOS EXTERNOS # ═══════════════════════════════════════════════════════════════════ REDIS_URL=redis://localhost:6379 SMTP_HOST= SMTP_PORT=587 SMTP_USER= SMTP_PASSWORD= # ═══════════════════════════════════════════════════════════════════ # CONFIGURACION DE LOGGING # ═══════════════════════════════════════════════════════════════════ LOG_LEVEL=debug # trace|debug|info|warn|error LOG_FORMAT=pretty # pretty|json ``` --- ## 3. BACKEND (NestJS) ### Schema de Validacion con Joi ```typescript // src/shared/config/config.validation.ts import * as Joi from 'joi'; export const configValidationSchema = Joi.object({ // Database DB_HOST: Joi.string().required(), DB_PORT: Joi.number().default(5432), DB_NAME: Joi.string().required(), DB_USER: Joi.string().required(), DB_PASSWORD: Joi.string().required(), DB_SSL: Joi.boolean().default(false), // JWT JWT_SECRET: Joi.string().min(32).required(), JWT_EXPIRATION: Joi.string().default('1d'), JWT_REFRESH_EXPIRATION: Joi.string().default('7d'), // Server NODE_ENV: Joi.string() .valid('development', 'staging', 'production', 'test') .default('development'), PORT: Joi.number().default(3000), API_PREFIX: Joi.string().default('api'), CORS_ORIGINS: Joi.string().default('*'), // Redis (opcional) REDIS_URL: Joi.string().uri().optional(), // SMTP (opcional) SMTP_HOST: Joi.string().optional(), SMTP_PORT: Joi.number().optional(), SMTP_USER: Joi.string().optional(), SMTP_PASSWORD: Joi.string().optional(), // Logging LOG_LEVEL: Joi.string() .valid('trace', 'debug', 'info', 'warn', 'error') .default('info'), LOG_FORMAT: Joi.string().valid('pretty', 'json').default('json'), }); ``` ### Configuracion Tipada ```typescript // src/shared/config/configuration.ts export interface DatabaseConfig { host: string; port: number; name: string; user: string; password: string; ssl: boolean; } export interface JwtConfig { secret: string; expiration: string; refreshExpiration: string; } export interface ServerConfig { nodeEnv: string; port: number; apiPrefix: string; corsOrigins: string[]; } export interface AppConfig { database: DatabaseConfig; jwt: JwtConfig; server: ServerConfig; redis?: { url: string; }; smtp?: { host: string; port: number; user: string; password: string; }; logging: { level: string; format: string; }; } export default (): AppConfig => ({ database: { host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT, 10), name: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, ssl: process.env.DB_SSL === 'true', }, jwt: { secret: process.env.JWT_SECRET, expiration: process.env.JWT_EXPIRATION, refreshExpiration: process.env.JWT_REFRESH_EXPIRATION, }, server: { nodeEnv: process.env.NODE_ENV, port: parseInt(process.env.PORT, 10), apiPrefix: process.env.API_PREFIX, corsOrigins: process.env.CORS_ORIGINS?.split(',') || ['*'], }, redis: process.env.REDIS_URL ? { url: process.env.REDIS_URL } : undefined, smtp: process.env.SMTP_HOST ? { host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT, 10), user: process.env.SMTP_USER, password: process.env.SMTP_PASSWORD, } : undefined, logging: { level: process.env.LOG_LEVEL, format: process.env.LOG_FORMAT, }, }); ``` ### Registro en AppModule ```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import configuration from './shared/config/configuration'; import { configValidationSchema } from './shared/config/config.validation'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [configuration], validationSchema: configValidationSchema, validationOptions: { abortEarly: true, // Fallar en primer error }, expandVariables: true, // Permitir ${VAR} en valores }), // ... otros modulos ], }) export class AppModule {} ``` ### Uso en Services ```typescript // src/modules/auth/services/auth.service.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtConfig } from '@/shared/config/configuration'; @Injectable() export class AuthService { private readonly jwtConfig: JwtConfig; constructor(private readonly configService: ConfigService) { // Acceso tipado a configuracion this.jwtConfig = this.configService.get('jwt'); } async generateToken(userId: string): Promise { return this.jwtService.sign( { sub: userId }, { secret: this.jwtConfig.secret, expiresIn: this.jwtConfig.expiration, }, ); } } ``` ### Configuraciones Especificas ```typescript // src/shared/config/database.config.ts import { registerAs } from '@nestjs/config'; import { TypeOrmModuleOptions } from '@nestjs/typeorm'; export default registerAs('database', (): TypeOrmModuleOptions => ({ type: 'postgres', host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT, 10), username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false, autoLoadEntities: true, synchronize: false, // NUNCA true en produccion logging: process.env.NODE_ENV === 'development', })); ``` ```typescript // src/shared/config/jwt.config.ts import { registerAs } from '@nestjs/config'; import { JwtModuleOptions } from '@nestjs/jwt'; export default registerAs('jwt', (): JwtModuleOptions => ({ secret: process.env.JWT_SECRET, signOptions: { expiresIn: process.env.JWT_EXPIRATION || '1d', }, })); ``` --- ## 4. FRONTEND (React/Vite) ### Variables de Entorno ```typescript // src/shared/config/env.ts // Vite expone variables con prefijo VITE_ interface EnvConfig { apiUrl: string; apiTimeout: number; environment: 'development' | 'staging' | 'production'; enableMockApi: boolean; sentryDsn?: string; gaTrackingId?: string; } function validateEnv(): EnvConfig { const apiUrl = import.meta.env.VITE_API_URL; const environment = import.meta.env.VITE_ENVIRONMENT || 'development'; // Validar variables requeridas if (!apiUrl) { throw new Error('VITE_API_URL is required'); } return { apiUrl, apiTimeout: parseInt(import.meta.env.VITE_API_TIMEOUT || '30000', 10), environment: environment as EnvConfig['environment'], enableMockApi: import.meta.env.VITE_ENABLE_MOCK_API === 'true', sentryDsn: import.meta.env.VITE_SENTRY_DSN, gaTrackingId: import.meta.env.VITE_GA_TRACKING_ID, }; } export const env = validateEnv(); // Helpers export const isDev = env.environment === 'development'; export const isProd = env.environment === 'production'; export const isStaging = env.environment === 'staging'; ``` ### Constantes de Aplicacion ```typescript // src/shared/config/constants.ts import { env } from './env'; export const APP_CONFIG = { // API API_URL: env.apiUrl, API_TIMEOUT: env.apiTimeout, // Paginacion DEFAULT_PAGE_SIZE: 20, MAX_PAGE_SIZE: 100, // UI TOAST_DURATION: 5000, DEBOUNCE_DELAY: 300, // Storage keys STORAGE_KEYS: { AUTH_TOKEN: 'auth_token', REFRESH_TOKEN: 'refresh_token', USER_PREFERENCES: 'user_preferences', THEME: 'theme', }, // Feature flags (pueden venir de API) FEATURES: { DARK_MODE: true, NOTIFICATIONS: true, BETA_FEATURES: env.environment !== 'production', }, } as const; // Rutas export const ROUTES = { HOME: '/', LOGIN: '/auth/login', REGISTER: '/auth/register', DASHBOARD: '/dashboard', PROFILE: '/profile', SETTINGS: '/settings', USERS: '/users', USER_DETAIL: '/users/:id', } as const; // API Endpoints export const API_ENDPOINTS = { AUTH: { LOGIN: '/auth/login', REGISTER: '/auth/register', REFRESH: '/auth/refresh', LOGOUT: '/auth/logout', }, USERS: { BASE: '/users', BY_ID: (id: string) => `/users/${id}`, ME: '/users/me', }, // ... otros endpoints } as const; ``` ### Uso en Componentes ```typescript // src/apps/web/hooks/useAuth.ts import { APP_CONFIG } from '@/shared/config/constants'; export const useAuth = () => { const login = async (credentials: LoginCredentials) => { const response = await api.post(API_ENDPOINTS.AUTH.LOGIN, credentials); // Guardar token usando key centralizada localStorage.setItem( APP_CONFIG.STORAGE_KEYS.AUTH_TOKEN, response.data.accessToken, ); }; return { login, /* ... */ }; }; ``` --- ## 5. DATABASE ### Configuracion de Conexion ```typescript // src/shared/config/typeorm.config.ts import { DataSource, DataSourceOptions } from 'typeorm'; import * as dotenv from 'dotenv'; dotenv.config(); export const dataSourceOptions: DataSourceOptions = { type: 'postgres', host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT, 10), username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false, // Entities entities: ['dist/**/*.entity.js'], // Migrations migrations: ['dist/migrations/*.js'], migrationsTableName: 'migrations', // Logging logging: process.env.DB_LOGGING === 'true', maxQueryExecutionTime: 1000, // Log queries > 1s // Pool extra: { max: parseInt(process.env.DB_POOL_SIZE || '10', 10), idleTimeoutMillis: 30000, connectionTimeoutMillis: 10000, }, }; // Para CLI de TypeORM export default new DataSource(dataSourceOptions); ``` --- ## 6. PATRON POR AMBIENTE ### Configuracion Condicional ```typescript // src/shared/config/by-environment.ts type Environment = 'development' | 'staging' | 'production' | 'test'; interface EnvironmentConfig { api: { rateLimit: number; timeout: number; }; cache: { ttl: number; enabled: boolean; }; features: { debugMode: boolean; mockExternalApis: boolean; }; } const configs: Record = { development: { api: { rateLimit: 1000, // Muy alto para desarrollo timeout: 60000, }, cache: { ttl: 60, enabled: false, // Deshabilitado para desarrollo }, features: { debugMode: true, mockExternalApis: true, }, }, staging: { api: { rateLimit: 100, timeout: 30000, }, cache: { ttl: 300, enabled: true, }, features: { debugMode: true, mockExternalApis: false, }, }, production: { api: { rateLimit: 60, timeout: 15000, }, cache: { ttl: 3600, enabled: true, }, features: { debugMode: false, mockExternalApis: false, }, }, test: { api: { rateLimit: 10000, timeout: 5000, }, cache: { ttl: 0, enabled: false, }, features: { debugMode: true, mockExternalApis: true, }, }, }; export function getEnvironmentConfig(): EnvironmentConfig { const env = (process.env.NODE_ENV || 'development') as Environment; return configs[env]; } ``` --- ## 7. SECRETOS Y SEGURIDAD ### Nunca en Codigo ```typescript // ❌ INCORRECTO: Secretos hardcodeados const JWT_SECRET = 'my-super-secret-key-12345'; const DB_PASSWORD = 'password123'; // ✅ CORRECTO: Desde variables de entorno const JWT_SECRET = process.env.JWT_SECRET; const DB_PASSWORD = process.env.DB_PASSWORD; ``` ### Validar Secretos Requeridos ```typescript // src/shared/config/secrets.validation.ts const REQUIRED_SECRETS = [ 'JWT_SECRET', 'DB_PASSWORD', ]; const OPTIONAL_SECRETS = [ 'SMTP_PASSWORD', 'STRIPE_SECRET_KEY', ]; export function validateSecrets(): void { const missing: string[] = []; for (const secret of REQUIRED_SECRETS) { if (!process.env[secret]) { missing.push(secret); } } if (missing.length > 0) { throw new Error( `Missing required secrets: ${missing.join(', ')}\n` + 'Please check your .env file or environment variables.', ); } // Validar formato de secretos if (process.env.JWT_SECRET && process.env.JWT_SECRET.length < 32) { throw new Error('JWT_SECRET must be at least 32 characters'); } } ``` ### Rotacion de Secretos ```typescript // Soportar multiples secretos para rotacion const JWT_SECRETS = (process.env.JWT_SECRETS || process.env.JWT_SECRET).split(','); // Verificar token con cualquier secreto valido async function verifyToken(token: string): Promise { for (const secret of JWT_SECRETS) { try { return jwt.verify(token, secret.trim()) as TokenPayload; } catch { continue; // Probar siguiente secreto } } throw new UnauthorizedException('Invalid token'); } // Firmar siempre con el primer secreto (mas reciente) function signToken(payload: TokenPayload): string { return jwt.sign(payload, JWT_SECRETS[0].trim()); } ``` --- ## 8. CHECKLIST DE CONFIGURACION ``` Setup inicial: [ ] .env.example creado con TODAS las variables [ ] .env en .gitignore [ ] Schema de validacion implementado [ ] App falla si configuracion invalida [ ] Tipos definidos para configuracion Seguridad: [ ] Ningun secreto en codigo fuente [ ] Ningun secreto en logs [ ] Secretos rotables (multiples valores soportados) [ ] Variables sensibles marcadas en documentacion Por ambiente: [ ] Configuracion diferenciada por ambiente [ ] Defaults seguros para produccion [ ] Modo debug deshabilitado en produccion [ ] Rate limiting apropiado por ambiente Documentacion: [ ] Cada variable documentada en .env.example [ ] README explica como configurar [ ] Variables opcionales vs requeridas claras ``` --- ## 9. ANTI-PATRONES ```typescript // ❌ ANTI-PATRON 1: Configuracion dispersa // archivo1.ts const API_URL = 'http://api.com'; // archivo2.ts const apiUrl = process.env.API_URL || 'http://api.com'; // ✅ CORRECTO: Centralizado // config/constants.ts export const API_URL = process.env.API_URL; ``` ```typescript // ❌ ANTI-PATRON 2: No validar const port = process.env.PORT; // Puede ser undefined o string invalido server.listen(port); // Error en runtime // ✅ CORRECTO: Validar y convertir const port = parseInt(process.env.PORT, 10); if (isNaN(port)) { throw new Error('PORT must be a valid number'); } ``` ```typescript // ❌ ANTI-PATRON 3: Defaults inseguros const DEBUG = process.env.DEBUG || true; // Debug activo por default // ✅ CORRECTO: Defaults seguros const DEBUG = process.env.DEBUG === 'true'; // False por default ``` --- **Version:** 1.0.0 | **Sistema:** SIMCO | **Tipo:** Patron de Codigo