Inicio / Artículos / Patrones anti-frontend: 7 errores costosos y sus soluciones prácticas

Patrones anti-frontend: 7 errores costosos y sus soluciones prácticas

Aprenda cómo detectar y corregir siete errores comunes en la ingeniería de backend, desde controladores sobrecargados hasta errores no manejados, antes de que causen problemas en producción.

2073 palabras

Cuando se empieza a desarrollar en el backend, es fácil asumir que el verdadero desafío radica en dominar más herramientas.

Node.js.

Express.

PostgreSQL.

Redis.

Docker.

Colas de mensajes.

Diseño de sistemas.

Pero después de lanzar varias aplicaciones, se hace evidente una verdad diferente.

Saber más herramientas no te convierte automáticamente en un ingeniero de backend mejor.

El mayor crecimiento proviene de cometer errores, averiguar por qué ocurrieron y asegurarse de que no se repitan.

A continuación se presentan siete errores comunes en el backend de los que aprender, junto con el enfoque que funciona mejor.

1. Colocar todo dentro del controlador

Este suele ser uno de los primeros errores que se cometen.

Un endpoint podría empezar así:

app.post("/orders", async (req, res) => {
  const { userId, productId, quantity } = req.body;
  const user = await db.users.findUnique({
    where: { id: userId }
  });  if (!user) {
    return res.status(404).json({
      message: "User not found"
    });
  }  const product = await db.products.findUnique({
    where: { id: productId }
  });  if (!product) {
    return res.status(404).json({
      message: "Product not found"
    });
  }  if (product.stock < quantity) {
    return res.status(400).json({
      message: "Not enough stock"
    });
  }  const order = await db.orders.create({
    data: {
      userId,
      productId,
      quantity
    }
  });  await sendEmail(user.email);  return res.status(201).json(order);
});

Funciona.

Pero mire todo lo que ahora maneja esta función:

  • Validación
  • Consultas a la base de datos
  • Reglas de negocio
  • Verificación de existencias
  • Creación de pedidos
  • Correos electrónicos
  • Respuestas HTTP

A medida que un proyecto crece, las funciones con tantas responsabilidades se convierten en bloques de lógica difíciles de gestionar.

La solución es dividir las responsabilidades.

Request
   ↓
Controller
   ↓
Service
   ↓
Repository
   ↓
Databas

La tarea del controlador es manejar HTTP.

La capa de servicio se encarga de la lógica de negocio.

La capa de repositorio se ocupa del acceso a los datos.

Eso no significa que una aplicación pequeña necesite seis capas de abstracción.

Significa que cada parte del sistema debe tener una tarea claramente definida.

2. Confiar en el frontend

Este error puede introducir errores y, peor aún, vulnerabilidades de seguridad.

Supongamos que el frontend envía este payload:

{
  "price": 10,
  "quantity": 2
}

Es tentador usar simplemente el precio enviado por el cliente para calcular el total del pedido.

No lo haga.

Nada impide que un cliente malintencionado envíe:

{
  "price": 1,
  "quantity": 100
}

El frontend está bajo el control del usuario, no el suyo.

Su backend debe validar y hacer cumplir las reglas que realmente importan.

Por ejemplo:

const product = await productRepository.findById(
  productId
);
const total = product.price * quantity;

El backend, y no el cliente, debe ser la fuente de verdad en cuanto a los precios.

Esta misma precaución debe extenderse a varias otras áreas que el cliente podría intentar influir:

  • Rollos de usuario
  • Permisos
  • Descuentos
  • Inventario
  • Montos de pago
  • Estatus de la cuenta
  • Propiedad de recursos

Considere el frontend como una herramienta para dar forma a la experiencia que los usuarios tienen con su producto.

No se trata de un límite de seguridad.

3. Manejo deficiente de errores

Al principio, el manejo de errores solía verse así:

try {
  // something
} catch (error) {
  console.log(error);
  return res.status(500).json({
    message: "Something went wrong"
  });
}

No hay nada intrínsecamente incorrecto en utilizar un mecanismo de respaldo general.

El problema radica en depender de él para todas las situaciones.

La ausencia de un registro de usuario no necesariamente constituye un error de nivel 500.

Una solicitud mal formada no necesariamente constituye un error de nivel 500.

Un correo electrónico duplicado no necesariamente constituye un error de nivel 500.

El backend debe poder distinguir entre diferentes tipos de fallos.

Por ejemplo:

400 → Invalid request
401 → Authentication required
403 → Not allowed
404 → Resource not found
409 → Conflict
422 → Validation failure
500 → Unexpected server error

Los códigos de estado específicos que elijas dependen de las convenciones de tu API, pero mantener la consistencia es más importante que el esquema exacto.

También son útiles las respuestas de errores estructuradas.

Por ejemplo:

{
  "success": false,
  "message": "User already exists",
  "code": "USER_ALREADY_EXISTS"
}

Con este formato, el frontend no necesita adivinar qué salió mal.

4. Codificación fija de la configuración

Este error parece inofensivo hasta que intentas desplegarlo.

Algo como:

const databaseUrl =
  "postgresql://user:password@localhost:5432/app";

O bien:

const jwtSecret = "my-secret";

Evita por completo este patrón.

Cada entorno en el que ejecutas tu aplicación necesita sus propios ajustes.

Podrías tener:

Development
    ↓
localhost
Staging
    ↓
staging databaseProduction
    ↓
production database

En su lugar, utiliza una configuración basada en el entorno:

DATABASE_URL=
REDIS_URL=
JWT_SECRET=
PAYMENT_API_KEY=
EMAIL_API_KEY=

Y nunca guardes datos confidenciales en el control de versiones.

Un archivo .env es adecuado para el desarrollo local, pero los entornos de producción requieren herramientas adecuadas para gestionar secretos y configuraciones.

El principio fundamental aquí es:

Tu código no debería estar estrechamente vinculado a valores de configuración específicos del entorno.

5. Escalado antes de que realmente sea necesario

Esta es una trampa en la que muchos desarrolladores caen en algún momento, y es fácil justificarla en ese instante.

Un equipo inicia un nuevo proyecto y de inmediato piensa en:

“¿Qué pasará si mañana aparecen 10 millones de personas?”

Así que deciden incluir:

Microservices
Kafka
Redis
Kubernetes
Multiple databases
API Gateway
Event-driven architecture

Sobre el papel, el sistema puede manejar una escala masiva.

Pero en realidad solo hay cinco usuarios.

Eso no es una arquitectura sólida. Es complejidad que nadie necesita aún.

Para la mayoría de los proyectos, tiene más sentido empezar simple:

Client
  ↓
Node.js Application
  ↓
PostgreSQL

Y solo incorporar nuevas partes cuando haya una razón concreta para hacerlo:

Need caching?
→ Redis
Need background jobs?
→ Queue + WorkerNeed more API capacity?
→ Multiple instances + Load BalancerDatabase becoming a bottleneck?
→ Optimize queries / indexes / architecture

Dejar que la arquitectura crezca junto con las necesidades reales y demostradas.

No recurrir a un sistema distribuido solo porque algún video afirma que eso es lo que hacen los ingenieros senior “de verdad”.

6. Bloqueo de la solicitud en tareas lentas

Esto puede arruinar silenciosamente la experiencia de uso de una API.

Imagínese esta configuración:

app.post("/order", async (req, res) => {
  const order = await createOrder();  await sendEmail();  await generateInvoice();  await notifyWarehouse();  await updateAnalytics();  return res.json(order);
});

Aquí, el usuario espera a que los cinco pasos se completen en secuencia.

Si incluso una sola llamada externa tarda cinco segundos adicionales, toda la respuesta se vuelve cinco segundos más lenta.

Un enfoque mejor es realizar únicamente dentro de la solicitud aquellas tareas que realmente deben hacerse de inmediato.

Todo lo demás puede ser colocado en una cola para su procesamiento en segundo plano.

Client
  ↓
API
  ↓
Create Order
  ↓
Queue Jobs
  ↓
Response

Seguido de:

Queue
  ↓
Worker
  ├── Send Email
  ├── Generate Invoice
  ├── Notification
  └── Analytics

Este es exactamente el tipo de escenario en el que herramientas como BullMQ combinadas con Redis demuestran su utilidad.

Pero hay una segunda lección fácil de pasar por alto:

Las tareas en segundo plano deben ser idempotentes y manejar las fallas de manera adecuada.

Si una tarea se ejecuta accidentalmente dos veces, los riesgos incluyen:

  • Cobrar al cliente más de una vez
  • Enviar notificaciones duplicadas
  • Escribir registros duplicados en la base de datos

Solo transferir el trabajo a una cola no resuelve este problema por sí solo.

La propia tarea debe diseñarse teniendo esto en cuenta.

7. Trabajar a ciegas en producción

Este error suele permanecer invisible hasta que algo realmente falla.

Imagínese que una API en producción comienza de repente a generar errores.

A primera vista, el servidor parece estar bien.

El código también parece correcto.

Pero esto es lo que falta:

No useful logs
No request IDs
No metrics
No error tracking
No database monitoring

En este punto, la única opción que queda es adivinar.

Depurar se convierte en una serie de encogimientos de hombros:

“¿Tal vez Redis dejó de funcionar? ¿Quizás la base de datos se ralentizó mucho? ¿O acaso el proveedor de pagos está teniendo problemas?”

Es una situación incómoda para un ingeniero.

Como mínimo, los registros útiles son esenciales.

Por ejemplo:

{
  "level": "error",
  "requestId": "req_123",
  "route": "/orders",
  "userId": "user_456",
  "message": "Payment provider timeout"
}

Con ese tipo de salida, se puede rastrear con precisión qué salió mal y dónde.

A medida que los sistemas crecen, la capacidad de observación generalmente se expande para incluir:

  • Registros de la aplicación
  • Rastreo de errores
  • Uso de CPU y memoria
  • Métricas a nivel de base de datos
  • Tiempos de respuesta de la API
  • Tamaño del retraso en las colas
  • Fallas de APIs externas
  • Puntos finales de verificación del estado

Un sistema no puede mantenerse saludable si no hay visibilidad sobre su comportamiento.

La lección más importante

Si lo analizamos con más detenimiento, casi todos estos problemas se deben al mismo hábito subyacente.

La pregunta que guía muchas decisiones suele ser:

"¿Funciona esta función?"

Mientras que en realidad debería ser:

"¿Es fácil modificar, depurar y ejecutar esta función en producción?"

Ese cambio en la forma de plantearlo modifica casi todo acerca de cómo se desarrolla el software.

Qué verificar antes de considerar que una API está lista

Antes de marcar como completa una función del backend, es útil realizar las siguientes verificaciones.

Código

  • ¿Cada capa tiene una responsabilidad clara y única?
  • ¿Se puede probar la lógica de negocio de forma aislada?
  • ¿Los controladores se mantienen lo suficientemente ligeros?

Seguridad

  • ¿Se valida toda la entrada recibida?
  • ¿Se aplican verificaciones de permisos en el lado del servidor?
  • ¿Los datos confidenciales están fuera del alcance de terceros?
  • ¿Están implementadas correctamente la autenticación y la autorización?
  • Banco de datos

    • ¿Son eficientes las consultas?
    • ¿Existen los índices adecuados?
    • ¿Se utilizan transacciones donde son necesarias?
    • ¿Existe un problema oculto de consultas N+1?

    Rendimiento

    • ¿Se han eliminado las llamadas secuenciales evitables?
    • ¿El trabajo intensivo se realiza en un proceso en segundo plano?
    • ¿Mejoraría algo aquí el uso del caché?

    Fiabilidad

    • ¿Cuál es la solución de respaldo si una API externa falla?
    • ¿Están configuradas las reintentos de manera adecuada?
    • ¿Es seguro ejecutar tareas en segundo plano más de una vez?
    • ¿Qué sucede si Redis o el banco de datos se vuelven inaccesibles?

    Operaciones

    • ¿Se puede saber qué sucedió cuando algo falla?
    • ¿Son realmente útiles los registros de actividad?
    • ¿Existen verificaciones de estado?
    • ¿Se puede medir el rendimiento de la API?

    Un sistema impecable no es el objetivo.

    Pero saber qué ocurre en cuanto algo sale mal es esencial.

    Los 7 errores de un vistazo

    Error Enfoque mejor
    Todo amontonado en los controladores Distribuir las responsabilidades entre capas
    Confiar en los datos del frontend Validar todo en el lado del servidor
    Manejo de errores genérico Utilizar una estrategia de errores consistente
    Secretos codificados directamente Gestión adecuada del entorno y la configuración
    Escalar demasiado pronto Escalar en respuesta a cuellos de botella reales
    Tareas lentas que bloquean las solicitudes Transferirlas a tareas en segundo plano
    Falta de información sobre el entorno de producción
    Agregar registro y monitoreo

    Conclusión final

    Existe la creencia común de que mejorar como desarrollador backend significa simplemente adquirir más herramientas y tecnologías.

    Una visión más precisa es que se trata realmente de comprender los compromisos que implica.

    ¿Debería esta operación ser síncrona o asíncrona?

    ¿Vale la pena cachear estos datos?

    ¿Necesita esta consulta un índice?

    ¿Debería convertirse en un servicio independiente?

    ¿Cuál es el plan si Redis deja de funcionar?

    ¿Cuál es el plan si la base de datos se vuelve extremadamente lenta?

    ¿Cuál es el plan si una tarea se ejecuta accidentalmente dos veces?

    ¿Cuál es el plan si la API externa en la que se depende falla?

    Saber las respuestas a estas preguntas es mucho más importante que simplemente saber cómo instalar otro paquete.

    Dado que los sistemas de producción no se evalúan por su comportamiento cuando todo funciona bien.

    La verdadera ingeniería se demuestra en el momento en que surgen problemas.

    Cada error comprendido hoy significa un incendio en la producción menos que habrá que apagar más adelante.

    Lecturas relacionadas

  • Elegir entre Promise.all, Promise.race y esperas secuenciales — Aprenda cuándo Promise.all() acelera las APIs de Node.js, por qué falla rápidamente ante cualquier rechazo, y un marco de toma de decisiones para elegir el patrón asíncrono adecuado.
  • Por qué los patrones de regex con retroceso pueden hacer colgar en silencio su servidor — Aprenda cómo la coincidencia codiciosa y el retroceso catastrófico pueden convertir una regex aparentemente correcta en un cuello de botella para la CPU que provoque interrupciones en producción, y cómo detectar dicho riesgo.
  • REST vs GraphQL: Los reales intercambios detrás de cada arquitectura — Explica los problemas específicos que resuelven REST y GraphQL, sus mecanismos internos y los intercambios ocultos que deben considerarse antes de elegir uno para su API.
  • Más allá del P95: Medir la latencia que realmente experimentan tus usuarios — Por qué un P95 saludable puede coexistir con un producto lento, cómo el tiempo de espera en colas y la propagación se ocultan en los paneles de control, y cómo el cronometraje por paso pone fin a las acusaciones sobre la latencia.
  • Seis patrones de integración para conectar servicios Node.js de forma confiable — Conozca los patrones fundamentales detrás de integraciones robustas en Node.js: solicitud-respuesta, sondeo, webhooks, clave API, autenticación JWT y OAuth, reintentos con retroceso temporal y mapeo de datos.