Todos los desarrolladores han leído "Clean Code" de Robert Martin. Pocos equipos aplican de verdad sus principios de forma consistente. La distancia entre saber cómo es el código limpio y entregarlo bajo la presión de los plazos es donde se degradan la mayoría de las bases de código.
En Privum hemos destilado años de experiencia en producción en 12 reglas que aplicamos en todos los proyectos — desde herramientas internas hasta plataformas de cara al cliente que gestionan millones de peticiones. No son pautas aspiracionales; son prácticas concretas respaldadas por revisiones de código, linters y gates de CI.
1. Las funciones hacen una sola cosa
Una función debe hacer una cosa, hacerla bien y hacer solo eso. Si necesitas la palabra "y" para describir lo que hace una función, deberían ser dos funciones.
Mal:
validateAndSaveUser(data) — esto valida Y guarda. Si la validación falla, ¿intenta guardar igualmente? Si el guardado falla, ¿revierte el estado de la validación? El acoplamiento crea ambigüedad.
Bien:
validateUser(data) → saveUser(validatedUser) — cada función tiene una única responsabilidad, un único motivo para fallar y una única cosa que probar.
La regla práctica: si una función tiene más de 20 líneas, probablemente hace más de una cosa. Extrae la segunda.
2. Nombra las cosas por lo que hacen, no por lo que son
Los nombres de variables y funciones deben revelar la intención. Quien lea el código debe entender qué hace sin leer la implementación.
Mal: const d = new Date() — ¿qué es "d"? Una fecha, pero ¿qué fecha? ¿Para qué la necesitamos?
Bien: const subscriptionExpiresAt = new Date(user.trialEnd) — ahora cualquiera que lo lea sabe exactamente qué representa y por qué existe.
Para variables booleanas, usa prefijos que se lean como preguntas: isActive, hasPermission, canDelete, shouldRetry. El código que se lee como inglés es código que no necesita comentarios.
3. Falla rápido, falla ruidosamente
No te tragues los errores en silencio. No devuelvas null cuando algo va mal. No registres un error en el log y sigas como si nada hubiera pasado.
Mal: Un try/catch que lo captura todo y devuelve un array vacío — quien llama no tiene ni idea de que algo ha fallado, y depurar se convierte en una pesadilla.
Bien: Valida las entradas en la frontera (handlers de API, envíos de formularios), lanza errores tipados cuando se violan invariantes y deja que el error se propague hasta un handler que sepa cómo responder (devolver un 400, mostrar un toast, reintentar).
Los fallos silenciosos son los bugs más caros. No tumban tu aplicación — corrompen tus datos poco a poco durante semanas hasta que alguien se da cuenta.
4. Escribe tests de comportamiento, no de implementación
Los tests deben verificar qué hace el código, no cómo lo hace. Si refactorizas las tripas y tus tests se rompen, tus tests están probando lo que no deben.
Mal: Comprobar que un método privado concreto se llamó con unos argumentos concretos — este test está acoplado a detalles de implementación y se romperá con cualquier refactor.
Bien: Dada una entrada, comprueba la salida o el efecto secundario. "Cuando un usuario envía un formulario válido, debe ver un mensaje de éxito y los datos deben quedar persistidos." Este test sobrevive a la refactorización porque prueba comportamiento.
Las métricas de cobertura son útiles pero engañosas. Un 80% de cobertura con buenos tests de comportamiento es mejor que un 100% de cobertura con tests frágiles de implementación.
5. Mantén las dependencias en los bordes
La lógica de negocio no debe importar clientes HTTP, drivers de base de datos ni código específico de un framework. Lleva el I/O a los bordes y mantén el núcleo puro.
No es un consejo académico — tiene consecuencias prácticas: - La lógica de negocio pura es trivial de probar (sin necesidad de mocks) - Pasar de PostgreSQL a MongoDB cambia el adaptador, no el dominio - Las actualizaciones del framework no se propagan en cascada por toda tu base de código
El patrón: Controladores/Handlers → Casos de uso/Servicios → Lógica de dominio. Las dependencias fluyen hacia dentro. El dominio nunca importa nada de la infraestructura.
6. Prefiere la composición a la herencia
La herencia crea un acoplamiento fuerte. Cuando heredas de una clase base, heredas todo su contrato — incluidas partes que no necesitas y bugs que no escribiste.
La composición te da flexibilidad: combina piezas pequeñas y enfocadas en lugar de construir jerarquías profundas. En TypeScript/JavaScript, esto significa preferir funciones y objetos simples a jerarquías de clases.
Si te encuentras creando una jerarquía de clases de más de dos niveles, para y refactoriza. La complejidad no compensa la abstracción.
7. Gestiona los errores en el nivel adecuado
No todas las funciones deben gestionar todos los errores. Los errores deben gestionarse en el nivel que tiene suficiente contexto para tomar la decisión correcta.
Una función de consulta a base de datos no debe decidir qué código de estado HTTP devolver — no sabe que la está llamando un handler HTTP. Debe lanzar un error tipado que el handler pueda interpretar.
Organiza la gestión de errores por capas: - Capa de dominio: lanza errores específicos del dominio (UserNotFound, InsufficientBalance) - Capa de aplicación: captura los errores de dominio y decide la estrategia de respuesta - Capa de infraestructura: captura los errores de transporte, implementa reintentos y circuit breakers
8. Haz que los estados ilegales sean irrepresentables
Usa tu sistema de tipos para prevenir bugs en tiempo de compilación. Si un usuario puede estar "activo" o "suspendido", no uses un booleano — usa un tipo unión o un enum. Si un pedido debe tener al menos un artículo, no uses un array — usa un tipo que garantice que no está vacío.
Ejemplo en TypeScript: en lugar de { status: string }, usa { status: 'active' | 'suspended' | 'deleted' }. Ahora el compilador detecta erratas y estados inválidos antes del runtime.
Cuantos más invariantes codifiques en los tipos, menos comprobaciones en tiempo de ejecución necesitas y menos bugs llegan a producción.
9. Las revisiones de código no son opcionales
Cada línea de código que llega a producción debe revisarla al menos otro ingeniero. Sin excepciones — ni para los "arreglos rápidos", ni para los "solo son cambios de configuración", ni para el tech lead.
Las revisiones de código detectan bugs, comparten conocimiento, hacen cumplir los estándares y crean una propiedad compartida del código. Son la práctica de calidad más eficaz después de los tests automatizados.
Mantén las revisiones pequeñas (menos de 400 líneas). Revisa en menos de 24 horas. Comenta patrones, no preferencias. Haz preguntas en lugar de exigencias.
10. Elimina el código muerto
El código muerto no es gratis. Confunde a los nuevos miembros del equipo, aumenta la carga cognitiva y, de vez en cuando, se reactiva por accidente. Si el código no se llama, bórralo. Git lo recuerda.
Esto incluye: - Bloques de código comentados - Imports y variables sin usar - Feature flags que llevan meses permanentemente activados o desactivados - Endpoints de API que nadie llama
Ejecuta una herramienta de análisis de código muerto cada trimestre. Te sorprenderá cuánto se acumula.
11. Escribe logs con propósito
Registrarlo todo es tan inútil como no registrar nada. Unos buenos logs responden a: "¿qué pasó, cuándo, a quién y en qué contexto?"
Estructura tus logs como JSON con campos consistentes: timestamp, level, service, requestId, userId, action, duration, error. Así se pueden buscar y agregar.
Usa bien los niveles de log: - ERROR: algo ha fallado y requiere atención - WARN: ha pasado algo inesperado, pero se ha gestionado - INFO: eventos de negocio relevantes (un usuario se ha registrado, se ha procesado un pago) - DEBUG: detalles técnicos útiles durante el desarrollo (tiempos de consulta, aciertos de caché)
En producción, trabaja con nivel INFO. Activa DEBUG solo cuando investigues problemas concretos.
12. Automatiza tus estándares
Las reglas que dependen de la disciplina humana para cumplirse se incumplirán bajo presión. Codifica tus estándares en las herramientas:
- Linting: ESLint, Prettier o Biome para formato y estilo
- Comprobación de tipos: TypeScript en modo strict, sin
any - Pre-commit hooks: Husky + lint-staged para detectar problemas antes de que lleguen a CI
- Gates de CI: los tests, la comprobación de tipos y el lint deben pasar antes del merge
- Escaneo de dependencias: Renovate o Dependabot para actualizaciones automatizadas
Si una regla es lo bastante importante como para exigirla, es lo bastante importante como para automatizarla. Los revisores humanos deben centrarse en la lógica, la arquitectura y la claridad — no en la indentación ni en el orden de los imports.
Conclusión
El código limpio no es un lujo — es una decisión económica. Cada hora invertida en calidad del código ahorra diez horas de depuración, onboarding y mantenimiento. Los equipos que aplican estas prácticas entregan más rápido (no más lento), porque dedican menos tiempo a pelearse con su propia base de código y más a construir funcionalidades.
El mejor momento para adoptar estas prácticas es al inicio de un proyecto. El segundo mejor momento es ahora. Elige tres reglas de esta lista, añádelas a la definition of done de tu equipo y aplícalas en tu próximo sprint. La mejora es incremental — pero se acumula.