Principios de código limpio que realmente importan
No todo consejo sobre código limpio vale lo mismo: estos son los principios que reducen errores, aceleran la incorporación y sobreviven a los plazos.

Todo desarrollador ha leído, o al menos ha oído hablar de, "Clean Code". El problema es que la mayoría de los equipos lo tratan como una escritura sagrada en lugar de una caja de herramientas. Algunos principios se pagan solos desde el primer día. Otros añaden ceremonia que te frena sin un beneficio medible. Saber distinguir entre ambos es lo que separa a los ingenieros pragmáticos de los dogmáticos.
Nombrar bien es la inversión con mayor retorno
Los nombres deficientes son la mayor fuente de confusión en las bases de código. Una función bien nombrada elimina la necesidad de un comentario. Una variable mal nombrada obliga a cualquier lector futuro a hacer ingeniería inversa sobre su intención.
// ❌ What does this even mean?
const d = new Date();
const flag = process.env.FF_X;
function handle(x: unknown) {
/* ... */
}
// ✅ Names carry intent — no comment needed
const subscriptionExpiresAt = new Date();
const isNewCheckoutEnabled = process.env.FF_NEW_CHECKOUT === "true";
function validatePaymentPayload(raw: unknown) {
/* ... */
}La regla es simple: si necesitas un comentario para explicar qué hace una variable o función, el nombre está mal. Renómbrala hasta que el comentario sea redundante.
Funciones pequeñas, límites claros
Las funciones de más de 30 líneas casi siempre hacen más de una cosa. Cuando una función se encarga de la validación, la transformación y la persistencia en un solo bloque, cada cambio termina tocando lógica que no tiene relación.
// ❌ One function doing three jobs
async function createOrder(input: OrderInput) {
if (!input.items.length) throw new Error("Empty cart");
if (!input.customerId) throw new Error("Missing customer");
const items = input.items.map((i) => ({
productId: i.id,
quantity: i.qty,
price: i.price * (1 - (i.discount ?? 0)),
}));
const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const order = await db.orders.create({
data: { customerId: input.customerId, items, total },
});
await emailService.send(input.customerId, order.id);
return order;
}
// ✅ Each function has one job
function validateOrderInput(input: OrderInput): void {
if (!input.items.length) throw new Error("Empty cart");
if (!input.customerId) throw new Error("Missing customer");
}
function calculateLineItems(items: CartItem[]): LineItem[] {
return items.map((i) => ({
productId: i.id,
quantity: i.qty,
price: i.price * (1 - (i.discount ?? 0)),
}));
}
async function persistOrder(customerId: string, items: LineItem[]) {
const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
return db.orders.create({ data: { customerId, items, total } });
}Cada función se puede probar de forma aislada. Cuando cambia la lógica de descuentos, solo tocas calculateLineItems. Cuando la capa de persistencia migra, solo tocas persistOrder.
Elimina el código muerto sin piedad
El código comentado, los imports sin usar y las funciones utilitarias "por si acaso" son ruido. Generan señales falsas al buscar en el código, complican los diffs y erosionan la confianza en la base de código.
// ❌ Commented-out code that "might be needed later"
// function legacyAuth(token: string) {
// return jwt.verify(token, OLD_SECRET);
// }
// import { formatCurrency } from "./old-utils"; // unused
// ✅ Delete it. Git remembers.
// If you need it back, `git log -S "legacyAuth"` finds it instantly.El control de versiones existe exactamente para esto. Si cada desarrollador deja su código "por si acaso" en su lugar, la base de código se llena de capas arqueológicas que nadie entiende.
Cláusulas de guarda en vez de condicionales anidados
El anidamiento profundo es una de las formas más rápidas de hacer que el código sea ilegible. Las cláusulas de guarda aplanan la lógica y dejan claro cuál es el camino esperado.
// ❌ Deep nesting obscures the main logic
function processPayment(order: Order) {
if (order) {
if (order.status === "pending") {
if (order.total > 0) {
if (order.paymentMethod) {
return chargeCard(order);
} else {
throw new Error("No payment method");
}
} else {
throw new Error("Invalid total");
}
} else {
throw new Error("Order not pending");
}
} else {
throw new Error("No order");
}
}
// ✅ Guard clauses — fail fast, then proceed
function processPayment(order: Order) {
if (!order) throw new Error("No order");
if (order.status !== "pending") throw new Error("Order not pending");
if (order.total <= 0) throw new Error("Invalid total");
if (!order.paymentMethod) throw new Error("No payment method");
return chargeCard(order);
}La segunda versión se lee de arriba a abajo. Cada cláusula de guarda ocupa una sola línea. La lógica de negocio real — chargeCard(order) — destaca claramente al final.
La consistencia le gana a la ingeniosidad
Una base de código donde cada archivo sigue los mismos patrones es más fácil de recorrer que una donde cada desarrollador exhibe su estilo personal. La consistencia reduce la carga cognitiva.
// ❌ Mixed patterns in the same codebase
const getUser = async (id) => await db.users.findUnique({ where: { id } });
async function fetchOrder(orderId: string): Promise<Order> {
return db.orders.findUnique({ where: { id: orderId } });
}
// ✅ Pick one pattern, use it everywhere
async function getUser(id: string): Promise<User> {
return db.users.findUnique({ where: { id } });
}
async function getOrder(id: string): Promise<Order> {
return db.orders.findUnique({ where: { id } });
}Esto aplica a las convenciones de nombres, la estructura de archivos, la estrategia de manejo de errores y el orden de los imports. Automatízalo con linters y formatters para que los humanos no tengan que pensarlo.
Qué no sobreoptimizar
Algunos consejos de código limpio hacen más daño que bien cuando se aplican de forma dogmática:
| Consejo | Cuándo perjudica |
|---|---|
| "Ninguna función de más de 5 líneas" | Crea laberintos de indirección donde terminas rastreando llamadas por 10 archivos |
"Nunca uses else" | Retornos anticipados forzados que oscurecen lógica que debería ir junta |
| "Abstrae todo" | Helpers de un solo uso que añaden una capa sin reducir la complejidad |
| "100% de cobertura de tests" | Probar getters y mappers triviales desperdicia tiempo y frena el refactor |
El objetivo es la claridad, no la obediencia a reglas arbitrarias. Si una función de 40 líneas se lee con claridad de arriba a abajo sin ramificaciones, está bien. Si una función de dos líneas tiene un nombre engañoso, no lo está.
Puntos clave
- Nombrar bien es tu refactor de mayor impacto — renombra hasta que los comentarios dejen de ser necesarios
- Las funciones pequeñas con una sola responsabilidad son más fáciles de probar, revisar y reemplazar
- Elimina el código muerto sin dudar — el control de versiones es tu red de seguridad
- Las cláusulas de guarda aplanan la lógica y dejan claro el camino esperado
- La consistencia en toda la base de código importa más que cualquier solución ingeniosa individual
- Aplica los principios con pragmatismo — el código limpio es una herramienta, no una religión


