Next.js
Antipatrones frecuentes en Next.js
Reúne antipatrones comunes de Next.js relacionados con Server y Client Components, datos, seguridad, caché, estado, runtime y despliegue.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Reúne antipatrones comunes de Next.js relacionados con Server y Client Components, datos, seguridad, caché, estado, runtime y despliegue.
Un antipatrón no es una técnica siempre incorrecta. Es una solución que parece conveniente, pero oculta costes o riesgos que aparecen con datos reales, seguridad, navegación, escala o mantenimiento. Reconocerlo exige entender el contexto y la alternativa, no memorizar prohibiciones.
local convenience
→ hidden architectural cost
→ production symptom
→ diagnosis
→ safer boundary or explicit trade-off"use client";en pages, layouts y componentes que no necesitan interacción.
Server Components por defecto y Client Components pequeños para eventos, state, Effects y browser APIs.
Una SPA deliberadamente cliente o una migración temporal. Debe medirse y documentarse.
useEffect(() => {
fetch("/api/products").then(...);
}, []);HTML inicial vacío, waterfall después de hydration, loading manual y peor SEO.
Leer en Server Component y entregar datos iniciales. Usa cliente para polling, offline, realtime o interacción dependiente del navegador.
Server Component
→ fetch /api/orders
→ Route Handler
→ service
→ DBRed interna, serialización, auth duplicada y errores HTTP innecesarios.
Llamar el servicio/DAL directamente desde servidor.
Cuando existe una frontera HTTP real: mobile, terceros, webhook o contrato público.
"use server";
export async function deleteOrder(id: string) {
return db.order.delete({ where: { id } });
}La referencia es invocable y el ID puede manipularse.
Validar input, sesión, tenant, permiso, estado y transacción dentro de la operación.
Proxy puede comprobar una cookie y redirigir, pero no protege cada recurso ni Action.
Proxy como filtro temprano; DAL, Action y Handler como autoridad.
const project = await db.project.findUnique({ where: { id } });
if (project.organizationId !== session.organizationId) deny();La entidad ya fue consultada fuera de scope y puede reutilizarse o registrarse.
Incluye tenant y actor en la query autorizada.
"use cache";
return getProject(projectId);Mezcla tenants, permisos obsoletos o información privada.
Cachear DTO público, incluir scope completo o mantener lectura request-time.
revalidatePath("/", "layout");Tags específicas por entidad, colección y tenant.
router.refresh() solicita un RSC payload nuevo. Si el servidor conserva la misma cache, devuelve el mismo dato.
Invalida updateTag o revalidateTag y refresca solo cuando la vista lo necesita.
Colocar sesión, analytics, queries y providers en root afecta todas las rutas.
Layouts anidados y providers cercanos al consumo.
Vuelve dinámica una región amplia y reduce prerendering/cache.
Lee request context en la región mínima bajo Suspense. Pasa variantes seguras a componentes cacheados cuando sea posible.
Drivers incompatibles, límites de paquetes, DB distante y debugging complejo.
Elige runtime por compatibilidad, duración y cercanía a datos. Node es una opción correcta por defecto.
Mostrar “pago completado” antes del servidor crea una confirmación falsa.
Pending claro; optimismo solo para acciones reversibles y de alta probabilidad de éxito.
Server Component data
→ copy to global Context
→ stale duplicateDos fuentes de verdad y reconciliación manual.
Mantener server data en servidor; copiar solo cuando una interacción cliente necesita ownership explícito.
La URL es ideal para filtros compartibles, pero no para hover, draft sensible o estado efímero.
Clasifica ownership: URL, local state, cookie, server DB o external store.
Acoplamiento, rerenders, persistencia confusa y duplicación de server state.
State local por defecto; URL para navegación; store solo para coordinación transversal real.
Puede producir flashes, hydration mismatch o registry complejo.
CSS Modules, Tailwind o solución con integración App Router verificada.
Compiten por ancho de banda y retrasan LCP real.
Preload solo del recurso LCP conocido; lazy loading para el resto.
El navegador puede descargar una imagen de 100vw para una card de media columna.
Mantén sizes alineado con el layout CSS.
Un widget usado en una página carga en todo el sitio.
Scope mínimo, estrategia adecuada y consentimiento antes de cargar analytics/marketing.
Oculta una divergencia en vez de resolver el estado inicial.
Haz determinista el primer render, usa cookie/servidor o un patrón de two-pass solo cuando sea necesario.
Puede ocultar que una operación debía cachearse o estar bajo Suspense.
Decide freshness y usa el modelo correspondiente. Con Cache Components, expresa use cache o boundary dinámica local.
try {
redirect("/login");
} catch {
return { error: true };
}Interfiere con excepciones internas de control.
Ejecuta redirect fuera de catches que manejan errores de dominio.
{ "success": false }con status 200 rompe clientes, caches y métricas.
Usa status HTTP y esquema de error estable.
Puede duplicar pagos, emails o pedidos.
Idempotency keys, outbox y retries solo bajo semántica segura.
Todo valor queda visible en el bundle.
Mantén secrets server-only y expón únicamente configuración pública necesaria.
const body = (await request.json()) as CreateOrderInput;No valida nada.
Schema runtime y error seguro.
Puede incluir PII, campos internos, Dates no serializadas o relaciones enormes.
DTO por caso de uso y select mínimo.
Más carpetas/servicios no crean modularidad.
Introduce separación cuando ownership, escalado o reutilización lo justifican.
Confunde framework y plataforma.
Documenta APIs del framework y capacidades específicas del proveedor por separado.
Development no reproduce prerendering, cache, minificación ni plataforma.
next build, next start, preview y tests de producción.
Framework, DB, auth, UI y arquitectura cambian a la vez.
No puedes aislar regresiones ni revertir fácilmente.
Cambios pequeños, compatibles y observables.
Antes de llamar algo antipatrón pregunta:
Diseño saludable:
Server page
→ authorized DAL
→ cached public/shared data
→ dynamic personal regions
→ small client controls
→ Action validate/authorize/transaction
→ scoped invalidationEvita:
client page
→ effect fetch internal API
→ global store copy
→ Action trusts hidden tenantId
→ refresh whole approuter.refresh no garantiza frescura?use client amplio?Caso práctico: SaaS de pedidos e inventario aplica estas decisiones a un producto multi-tenant inspirado en DomiSys.