# ANTIPATRONES: Lo que NUNCA Hacer **Versión:** 1.0.0 **Fecha:** 2025-12-08 **Prioridad:** OBLIGATORIA - Consultar antes de implementar **Sistema:** SIMCO + CAPVED --- ## PROPÓSITO Documentar antipatrones comunes para que agentes y subagentes los eviten. Cada antipatrón incluye el problema, por qué es malo, y la solución correcta. --- ## 1. ANTIPATRONES DE DATABASE ### DB-001: Crear tabla sin schema ```sql -- ❌ INCORRECTO CREATE TABLE users ( id UUID PRIMARY KEY ); -- ✅ CORRECTO CREATE TABLE auth.users ( id UUID PRIMARY KEY ); ``` **Por qué es malo:** Sin schema, la tabla va a `public`, mezclando dominios y dificultando permisos. --- ### DB-002: Columna NOT NULL sin DEFAULT en tabla existente ```sql -- ❌ INCORRECTO (en tabla con datos) ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL; -- ✅ CORRECTO ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'active'; ``` **Por qué es malo:** Falla en INSERT para registros existentes que no tienen valor. --- ### DB-003: Foreign Key sin ON DELETE ```sql -- ❌ INCORRECTO CREATE TABLE orders ( user_id UUID REFERENCES users(id) ); -- ✅ CORRECTO CREATE TABLE orders ( user_id UUID REFERENCES users(id) ON DELETE CASCADE -- o ON DELETE SET NULL, ON DELETE RESTRICT según el caso ); ``` **Por qué es malo:** Al eliminar usuario, los orders quedan huérfanos o el DELETE falla sin explicación clara. --- ### DB-004: Usar TEXT cuando debería ser ENUM ```sql -- ❌ INCORRECTO CREATE TABLE users ( status TEXT -- Permite cualquier valor ); -- ✅ CORRECTO CREATE TYPE user_status AS ENUM ('active', 'inactive', 'suspended'); CREATE TABLE users ( status user_status DEFAULT 'active' ); -- O con CHECK constraint CREATE TABLE users ( status VARCHAR(20) CHECK (status IN ('active', 'inactive', 'suspended')) ); ``` **Por qué es malo:** TEXT permite valores inválidos, causando bugs silenciosos. --- ### DB-005: Índice faltante en columna de búsqueda frecuente ```sql -- ❌ INCORRECTO CREATE TABLE products ( sku VARCHAR(50) UNIQUE -- Sin índice adicional para búsquedas ); -- ✅ CORRECTO CREATE TABLE products ( sku VARCHAR(50) UNIQUE ); CREATE INDEX idx_products_sku ON products(sku); -- UNIQUE ya crea índice, pero para búsquedas parciales: CREATE INDEX idx_products_sku_pattern ON products(sku varchar_pattern_ops); ``` **Por qué es malo:** Queries lentas en tablas grandes (full table scan). --- ### DB-006: Guardar contraseñas en texto plano ```sql -- ❌ INCORRECTO CREATE TABLE users ( password VARCHAR(100) -- Almacena "mipassword123" ); -- ✅ CORRECTO CREATE TABLE users ( password_hash VARCHAR(255) -- Almacena hash bcrypt ); -- Hashing se hace en backend, no en SQL ``` **Por qué es malo:** Vulnerabilidad de seguridad crítica. --- ## 2. ANTIPATRONES DE BACKEND ### BE-001: Lógica de negocio en Controller ```typescript // ❌ INCORRECTO @Controller('orders') export class OrderController { @Post() async create(@Body() dto: CreateOrderDto) { // Lógica de negocio en controller const total = dto.items.reduce((sum, item) => sum + item.price * item.qty, 0); const tax = total * 0.16; const discount = dto.couponCode ? await this.calculateDiscount() : 0; // ... más lógica } } // ✅ CORRECTO @Controller('orders') export class OrderController { @Post() async create(@Body() dto: CreateOrderDto) { return this.orderService.create(dto); // Delega a service } } @Injectable() export class OrderService { async create(dto: CreateOrderDto) { const total = this.calculateTotal(dto.items); const tax = this.calculateTax(total); // Lógica de negocio en service } } ``` **Por qué es malo:** Controller difícil de testear, lógica no reutilizable, violación de SRP. --- ### BE-002: Query directo en Service sin Repository ```typescript // ❌ INCORRECTO @Injectable() export class UserService { async findActive() { // Query directo con QueryBuilder return this.connection.query(` SELECT * FROM users WHERE status = 'active' `); } } // ✅ CORRECTO @Injectable() export class UserService { constructor( @InjectRepository(UserEntity) private readonly repository: Repository, ) {} async findActive() { return this.repository.find({ where: { status: UserStatus.ACTIVE }, }); } } ``` **Por qué es malo:** No tipado, vulnerable a SQL injection, difícil de mantener. --- ### BE-003: Validación en Service en lugar de DTO ```typescript // ❌ INCORRECTO @Injectable() export class UserService { async create(dto: any) { if (!dto.email) throw new BadRequestException('Email required'); if (!dto.email.includes('@')) throw new BadRequestException('Invalid email'); if (dto.name.length < 2) throw new BadRequestException('Name too short'); // ... más validaciones manuales } } // ✅ CORRECTO export class CreateUserDto { @IsEmail() @IsNotEmpty() email: string; @IsString() @MinLength(2) name: string; } // Service recibe DTO ya validado ``` **Por qué es malo:** Validación duplicada, inconsistente, no documentada en Swagger. --- ### BE-004: Catch vacío o silencioso ```typescript // ❌ INCORRECTO async processPayment() { try { await this.paymentGateway.charge(); } catch (e) { // Silencioso - error perdido } } // ❌ TAMBIÉN INCORRECTO async processPayment() { try { await this.paymentGateway.charge(); } catch (e) { console.log(e); // Solo log, sin manejar } } // ✅ CORRECTO async processPayment() { try { await this.paymentGateway.charge(); } catch (error) { this.logger.error('Payment failed', { error, context: 'payment' }); throw new InternalServerErrorException('Error procesando pago'); } } ``` **Por qué es malo:** Errores silenciosos causan bugs imposibles de debuggear. --- ### BE-005: Entity no alineada con DDL ```typescript // DDL tiene: // status VARCHAR(20) CHECK (status IN ('active', 'inactive')) // ❌ INCORRECTO @Column() status: string; // No valida valores // ✅ CORRECTO enum UserStatus { ACTIVE = 'active', INACTIVE = 'inactive', } @Column({ type: 'varchar', length: 20 }) status: UserStatus; ``` **Por qué es malo:** Entity permite valores que BD rechaza, errores en runtime. --- ### BE-006: Hardcodear configuración ```typescript // ❌ INCORRECTO const API_URL = 'https://api.stripe.com/v1'; const DB_HOST = '192.168.1.100'; // ✅ CORRECTO const API_URL = this.configService.get('STRIPE_API_URL'); const DB_HOST = this.configService.get('DB_HOST'); // O usar decorador @Injectable() export class PaymentService { constructor( @Inject('STRIPE_CONFIG') private readonly config: StripeConfig, ) {} } ``` **Por qué es malo:** Difícil cambiar entre ambientes, secretos expuestos en código. --- ### BE-007: N+1 Query Problem ```typescript // ❌ INCORRECTO async getOrdersWithItems() { const orders = await this.orderRepository.find(); for (const order of orders) { order.items = await this.itemRepository.find({ where: { orderId: order.id } }); } return orders; } // Resultado: 1 query para orders + N queries para items // ✅ CORRECTO async getOrdersWithItems() { return this.orderRepository.find({ relations: ['items'], // JOIN en una query }); } // Resultado: 1 query con JOIN ``` **Por qué es malo:** Performance terrible en listas grandes (100 orders = 101 queries). --- ## 3. ANTIPATRONES DE FRONTEND ### FE-001: Fetch directo en componente ```typescript // ❌ INCORRECTO const UserProfile = () => { const [user, setUser] = useState(null); useEffect(() => { fetch('/api/users/me') .then(res => res.json()) .then(setUser); }, []); return
{user?.name}
; }; // ✅ CORRECTO // hooks/useUser.ts const useUser = () => { return useQuery({ queryKey: ['user', 'me'], queryFn: () => userService.getMe(), }); }; // components/UserProfile.tsx const UserProfile = () => { const { data: user, isLoading, error } = useUser(); if (isLoading) return ; if (error) return ; return
{user.name}
; }; ``` **Por qué es malo:** No cachea, no maneja loading/error, lógica no reutilizable. --- ### FE-002: Hardcodear URLs de API ```typescript // ❌ INCORRECTO const response = await fetch('http://localhost:3000/api/users'); // ✅ CORRECTO // config.ts export const API_URL = import.meta.env.VITE_API_URL; // services/api.ts const api = axios.create({ baseURL: API_URL, }); ``` **Por qué es malo:** Rompe en producción, difícil cambiar backend. --- ### FE-003: Estado global para todo ```typescript // ❌ INCORRECTO - Redux/Zustand para estado de formulario const formStore = create((set) => ({ name: '', email: '', setName: (name) => set({ name }), setEmail: (email) => set({ email }), })); // ✅ CORRECTO - Estado local para formularios const UserForm = () => { const [name, setName] = useState(''); const [email, setEmail] = useState(''); // O usar react-hook-form }; // Store global SOLO para: // - Auth state (usuario logueado) // - UI state (tema, sidebar abierto) // - Cache de server state (mejor usar React Query) ``` **Por qué es malo:** Complejidad innecesaria, renders innecesarios, difícil de seguir. --- ### FE-004: Props drilling excesivo ```typescript // ❌ INCORRECTO // Finalmente se usa // ✅ CORRECTO - Context para datos compartidos const UserContext = createContext(null); const useUser = () => useContext(UserContext); // Usa useUser() internamente ``` **Por qué es malo:** Componentes intermedios reciben props que no usan, refactoring doloroso. --- ### FE-005: useEffect para todo ```typescript // ❌ INCORRECTO const ProductList = ({ categoryId }) => { const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch(`/api/products?category=${categoryId}`) .then(res => res.json()) .then(data => { setProducts(data); setLoading(false); }); }, [categoryId]); // Race conditions, no cleanup, no error handling }; // ✅ CORRECTO - React Query const ProductList = ({ categoryId }) => { const { data: products, isLoading } = useQuery({ queryKey: ['products', categoryId], queryFn: () => productService.getByCategory(categoryId), }); // Maneja cache, loading, error, race conditions automáticamente }; ``` **Por qué es malo:** Race conditions, memory leaks, re-inventar la rueda. --- ### FE-006: Tipos no sincronizados con backend ```typescript // Backend DTO (actualizado) class UserDto { id: string; email: string; name: string; phone: string; // ← NUEVO CAMPO } // ❌ INCORRECTO - Frontend desactualizado interface User { id: string; email: string; name: string; // Falta phone → undefined en runtime } // ✅ CORRECTO - Mantener sincronizado interface User { id: string; email: string; name: string; phone?: string; // Sincronizado con backend } ``` **Por qué es malo:** Bugs silenciosos en runtime cuando backend cambia. --- ## 4. ANTIPATRONES DE ARQUITECTURA ### ARCH-001: Importar de capa incorrecta ```typescript // ❌ INCORRECTO - Controller importa de otro módulo directamente import { ProductEntity } from '../products/entities/product.entity'; // ✅ CORRECTO - Usar exports del módulo import { ProductService } from '../products/product.module'; // O mejor: dependency injection @Module({ imports: [ProductModule], }) export class OrderModule {} ``` **Por qué es malo:** Acopla módulos, rompe encapsulamiento, dependencias circulares. --- ### ARCH-002: Duplicar código entre módulos ```typescript // ❌ INCORRECTO - Misma función en dos módulos // users/utils.ts function formatDate(date: Date) { ... } // orders/utils.ts function formatDate(date: Date) { ... } // Duplicado // ✅ CORRECTO - Shared utils // shared/utils/date.utils.ts export function formatDate(date: Date) { ... } ``` **Por qué es malo:** Cambios deben hacerse en múltiples lugares, inconsistencias. --- ### ARCH-003: Circular dependencies ```typescript // ❌ INCORRECTO // user.service.ts import { OrderService } from '../orders/order.service'; // order.service.ts import { UserService } from '../users/user.service'; // ✅ CORRECTO - Usar forwardRef o reestructurar @Injectable() export class UserService { constructor( @Inject(forwardRef(() => OrderService)) private orderService: OrderService, ) {} } // O mejor: Extraer lógica compartida a un tercer servicio ``` **Por qué es malo:** Errores en runtime, código difícil de entender. --- ## 5. ANTIPATRONES DE TESTING ### TEST-001: Tests que dependen de orden ```typescript // ❌ INCORRECTO describe('UserService', () => { it('should create user', () => { const user = service.create({ email: 'test@test.com' }); // Asume que este test corre primero }); it('should find user', () => { const user = service.findByEmail('test@test.com'); // Depende del test anterior }); }); // ✅ CORRECTO - Tests independientes describe('UserService', () => { beforeEach(async () => { // Setup limpio para cada test await repository.clear(); }); it('should create user', () => { ... }); it('should find user', async () => { // Crear datos necesarios en el test await repository.save({ email: 'test@test.com' }); const user = await service.findByEmail('test@test.com'); }); }); ``` --- ### TEST-002: Mock incorrecto ```typescript // ❌ INCORRECTO - Mock parcial/inconsistente jest.mock('./user.service', () => ({ findOne: jest.fn().mockResolvedValue({ id: '1' }), // Otros métodos no mockeados → undefined })); // ✅ CORRECTO - Mock completo const mockUserService = { findOne: jest.fn(), findAll: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn(), }; beforeEach(() => { jest.clearAllMocks(); }); ``` --- ## 6. CHECKLIST DE REVISIÓN Antes de hacer commit, verificar que NO estás haciendo: ``` Database: [ ] Tabla sin schema [ ] NOT NULL sin DEFAULT en tabla existente [ ] FK sin ON DELETE [ ] TEXT donde debería ser ENUM [ ] Índices faltantes Backend: [ ] Lógica en Controller [ ] Queries directas sin Repository [ ] Validación en Service [ ] Catch vacío [ ] Entity desalineada con DDL [ ] Config hardcodeada [ ] N+1 queries Frontend: [ ] Fetch en componente [ ] URLs hardcodeadas [ ] Store global para estado local [ ] Props drilling excesivo [ ] useEffect innecesario [ ] Tipos desactualizados Arquitectura: [ ] Import de capa incorrecta [ ] Código duplicado [ ] Dependencias circulares Testing: [ ] Tests dependientes [ ] Mocks incompletos ``` --- **Versión:** 1.0.0 | **Sistema:** SIMCO | **Tipo:** Guía de Antipatrones