Escenarios comunes donde los desarrolladores de React y Next.js deben elegir entre patrones, hooks o enfoques arquitectónicos. Cada decisión presenta la mejor, segunda y tercera opción, más la opción incorrecta que los desarrolladores comúnmente eligen.
Crea un route handler GET y fetching desde un Server Component. Funciona pero añade un salto de red innecesario.
3ra
getServerSideProps (Pages Router)
Sigue funcionando si estás en Pages Router, pero pierdes beneficios de streaming y RSC.
Opción incorrecta: Usar useEffect + fetch en un Client Component. Añade un waterfall cliente-servidor, expone tu API y daña SEO porque el contenido no está en el HTML inicial.
Por qué la mejor es la mejor: Los Server Components asincronía fetching de datos con cero JS del cliente, flujo HTML progresivamente, y acceden a la base de datos directamente sin una capa API.
Escenario: Un usuario envía un formulario de contacto que necesita validación del servidor y visualización de errores.
Rango
Opción
Enfoque
Mejor
Server Action + useActionState
Vincula un Server Action a <form action={...}> y usa useActionState para estado pendiente/error.
2da
Server Action + estado manual
Llama la acción vía startTransition y gestiona tu propio estado con useState. Más boilerplate pero control total.
3ra
API route + client fetch
POST a un route handler desde un manejador onSubmit. Funciona pero omite la mejora progresiva.
Opción incorrecta: Validación solo del lado del cliente sin validación del servidor. Nunca confíes en el cliente -- los atacantes omiten tu UI.
Por qué la mejor es la mejor:useActionState te da estados pendiente, error y datos automáticamente, funciona sin JS (mejora progresiva), y mantiene la lógica de validación en el servidor.
Escenario: Tu dashboard tiene un sidebar y encabezado que deben persistir en /dashboard/analytics, /dashboard/settings, etc.
Rango
Opción
Enfoque
Mejor
layout.tsx anidado
Coloca un layout.tsx en app/dashboard/ con UI compartido. Las rutas hijo se renderizan dentro de {children}.
2da
Archivo template
Usa template.tsx en su lugar. Mismo anidamiento, pero se remonta en navegación (útil para animaciones).
3ra
Componente wrapper
Importa un wrapper <DashboardShell> en cada página. Funciona pero duplica el import y rompe la preservación automática de layout.
Opción incorrecta: Poner lógica de layout en _app.tsx (Pages Router) o un proveedor de contexto que re-renderiza todo el árbol. Esto derrota el propósito de layouts anidados.
Por qué la mejor es la mejor:layout.tsx se preserva entre navegaciones -- el sidebar no se re-renderiza cuando cambias de pestaña, preservando el estado de desplazamiento y evitando trabajo innecesario.
Escenario: Tu página de dashboard fetching datos de análisis pesados y necesitas un esqueleto de carga.
Rango
Opción
Enfoque
Mejor
loading.tsx
Añade un archivo loading.tsx junto a page.tsx. Next.js auto-envuelve la página en un límite <Suspense> con tu UI de carga.
2da
<Suspense> manual
Envuelve componentes asincronía específicos en <Suspense fallback={<Skeleton />}> para control granular.
3ra
Estado de carga del lado del cliente
Usa useState(true) y establece false después de que el fetch de useEffect se complete.
Opción incorrecta: No mostrar nada (pantalla en blanco) mientras se cargan datos. Los usuarios piensan que la app está rota después de ~300ms sin retroalimentación.
Por qué la mejor es la mejor:loading.tsx es automático, requiere cero JS del cliente, y permite navegación instantánea vía la arquitectura de streaming de React.
Escenario: Un componente gestiona un formulario de múltiples pasos con 8 campos, estado de validación y navegación de pasos.
Rango
Opción
Enfoque
Mejor
useReducer
Define acciones como SET_FIELD, NEXT_STEP, VALIDATE. Las transiciones de estado son explícitas y testables.
2da
useState con un objeto
const [form, setForm] = useState({...}). Más simple pero las actualizaciones basadas en spread se vuelven desordenadas con lógica compleja.
3ra
Múltiples llamadas useState
Una por campo. Está bien para formularios simples pero 8+ llamadas useState son difíciles de coordinar.
Opción incorrecta: Alcanzar una biblioteca de estado global (Redux, Zustand) para estado de formulario que solo vive en un componente. Massivo overkill.
Por qué la mejor es la mejor:useReducer centraliza transiciones de estado complejas en una función pura que puedes hacer pruebas unitarias independientemente de React.
Escenario: Tema, locale y preferencias del usuario deben ser accesibles 5+ niveles de profundidad.
Rango
Opción
Enfoque
Mejor
React Context
Crea un proveedor de contexto cerca de la parte superior. Los consumidores leen vía useContext o React 19's use(ThemeContext).
2da
Composición de componentes
Pasa los datos como children o render props para evitar que componentes intermedios necesiten la prop.
3ra
Prop drilling
Pasa props a través de cada nivel. Tedioso pero explícito y fácil de rastrear.
Opción incorrecta: Instalar una biblioteca de gestión de estado solo para evitar prop drilling. Context es built-in y suficiente para datos de lectura pesada, raramente cambiantes como temas.
Por qué la mejor es la mejor: Context es cero dependencias, construido en React, y optimizado para datos que cambian con poca frecuencia pero se leen ampliamente.
Establece estado inmediatamente, revierte en error en un try/catch. Más código, misma idea.
3ra
onMutate de React Query
Actualización optimista vía callbacks de mutación. Bueno pero tira en una biblioteca para algo que React 19 hace nativamente.
Opción incorrecta: Esperar respuesta del servidor antes de actualizar la UI. El retraso de 200-500ms hace que la app se sienta lenta.
Por qué la mejor es la mejor:useOptimistic automáticamente revierte al estado real cuando el Server Action se completa o falla -- sin lógica de rollback manual necesaria.
Escenario: La página /dashboard/billing puede fallar (errores de API de pago) y necesita un fallback elegante.
Rango
Opción
Enfoque
Mejor
error.tsx
Añade error.tsx junto a la página. Captura errores de renderizado y datos, ofrece un botón de reintentar vía reset().
2da
Componente error boundary
Envuelve en un <ErrorBoundary> personalizado para más control sobre tipos de error y reportes.
3ra
Try/catch en Server Component
Captura errores en la función asincronía y renderiza UI fallback inline. Sin reintentos automáticos.
Opción incorrecta: Dejar que los errores se propaguen a una página de error global que reemplaza todo el layout. El usuario pierde todo contexto y estado de navegación.
Por qué la mejor es la mejor:error.tsx está orientado al segmento de ruta -- el resto del layout permanece interactivo, y reset() reintenta renderizado sin recarga completa de página.
Escenario: Un componente muestra una tarjeta de producto con nombre, precio, imagen y un botón "Añadir al Carrito".
Rango
Opción
Enfoque
Mejor
Dividir: tarjeta Server + botón Client
Mantén ProductCard como un Server Component. Extrae <AddToCartButton> como un Client Component.
2da
Tarjeta entera como Client Component
Añade "use client" a la tarjeta. Simple pero envía más JS del necesario.
3ra
Server Component con wrapper de hidratación del lado del cliente
Envuelve la parte interactiva con un límite de hidratación genérico. Over-engineered para un botón.
Opción incorrecta: Añadir "use client" a cada componente "solo para estar seguro." Esto derrota beneficios de RSC y envía JavaScript innecesario.
Por qué la mejor es la mejor: Empuja el límite "use client" lo más bajo posible. Solo el botón interactivo necesita JS del cliente -- la tarjeta, imagen y texto se renderizan con cero JS.
Escenario: Los posts del blog se actualizan con poca frecuencia. Quieres cargas rápidas pero contenido fresco dentro de minutos.
Rango
Opción
Enfoque
Mejor
ISR con revalidate
export const revalidate = 60; en tu página o layout. Sirve HTML en caché, revalida en background.
2da
Revalidación bajo demanda
Llama revalidatePath('/blog/[slug]') o revalidateTag('posts') desde un webhook CMS. Frescura instantánea.
3ra
Totalmente estático (generateStaticParams)
Pre-renderiza todos los posts en construcción. Rápido pero obsoleto hasta el siguiente despliegue.
Opción incorrecta: Hacer la página de blog completamente dinámica (export const dynamic = 'force-dynamic'). Sin caching significa cada solicitud golpea la base de datos -- lento y desperdiciador para contenido que apenas cambia.
Por qué la mejor es la mejor: ISR basado en tiempo es cero-config, sirve contenido obsoleto mientras revalida, y balancea frescura con rendimiento automáticamente.
Escenario:/dashboard solo debe ser accesible a usuarios logged-in.
Rango
Opción
Enfoque
Mejor
Middleware
Verifica la sesión en middleware.ts y redirige antes de que la página se renderice. Cero flash de layout.
2da
Verificación de Server Component
Verifica auth en el Server Component de la página y llama redirect('/login'). Funciona pero el layout puede parpadearse.
3ra
Guard del lado del cliente
Verifica auth en useEffect y redirige. Muestra contenido protegido brevemente antes de redireccionar.
Opción incorrecta: Solo ocultar el link de navegación a /dashboard. Seguridad por obscuridad -- cualquiera con la URL puede acceder a la página.
Por qué la mejor es la mejor: Middleware se ejecuta en el edge antes de cualquier renderizado. El usuario nunca ve un flash de contenido protegido, y la verificación está centralizada para todas las rutas protegidas.
Built-in, sin dependencia. Bastante bueno para apps pequeñas pero re-renderiza todos los consumidores en cualquier cambio.
3ra
Redux Toolkit
Poderoso pero pesado para un carrito. Vale la pena solo si la app ya usa Redux para otro estado.
Opción incorrecta: Almacenar estado del carrito en un Server Component o enfoque solo-cookie sin reactividad del cliente. La UI no se actualizará cuando se añadan items sin recarga completa de página.
Por qué la mejor es la mejor: Zustand es ~1KB, no requiere proveedores, soporta selectores para evitar re-renders innecesarios, y funciona naturalmente con características concurrentes de React 18/19.
Envuelve cada sección en su propio <Suspense>. Se fluyen independientemente conforme se resuelven.
3ra
Awaits secuenciales
const user = await getUser(); const analytics = await getAnalytics(); -- simple pero crea un waterfall.
Opción incorrecta: Fetching de los tres en un único useEffect secuencialmente. Tres waterfalls más renderizado del lado del cliente hace que el dashboard se sienta dolorosamente lento.
Por qué la mejor es la mejor:Promise.all dispara las tres solicitudes simultáneamente. Tiempo total de espera = solicitud más lenta, no la suma de las tres.
Escenario: Iniciando una app Next.js greenfield y eligiendo una solución de estilo.
Rango
Opción
Enfoque
Mejor
Tailwind CSS
Utilidad-primero, cero runtime, funciona perfectamente con Server Components, ecosistema enorme (shadcn/ui).
2da
CSS Modules
Estilos orientados, sin runtime, built-in a Next.js. Bueno para equipos que prefieren CSS tradicional.
3ra
Vanilla Extract
CSS-in-TS type-safe con cero runtime. Más setup pero excelente para equipos de sistema de diseño.
Opción incorrecta: Usar una biblioteca CSS-in-JS con runtime (styled-components, Emotion) con Server Components. Requieren renderizado del lado del cliente y rompen streaming RSC.
Por qué la mejor es la mejor: Tailwind tiene costo cero de runtime, funciona con RSC, y combinado con shadcn/ui proporciona componentes accesibles listos para producción fuera de la caja.
<picture> manual con srcSet y un CDN como Cloudinary. Control total pero más trabajo.
3ra
<img> plano con lazy loading
<img loading="lazy" /> -- sin optimización, sizing manual, CLS potencial.
Opción incorrecta: Usar tags <img> no optimizados con imágenes de fuente resolución completa. Las páginas cargan megabytes de imágenes, matando Core Web Vitals.
Por qué la mejor es la mejor:next/image automáticamente sirve WebP/AVIF, cambia tamaño para ancho de dispositivo, lazy loads debajo del fold, y previene CLS con dimensiones requeridas.
Escenario: Tu app necesita consultar una base de datos PostgreSQL desde código del lado del servidor.
Rango
Opción
Enfoque
Mejor
Prisma en Server Components/Actions
Consulta directamente: const users = await prisma.user.findMany(). Type-safe, sin capa API necesaria.
2da
Drizzle ORM
Más ligero, sintaxis similar a SQL, excelente inferencia de TypeScript. Excelente para equipos que prefieren SQL.
3ra
SQL crudo vía pg o postgres
Control máximo, sin overhead de ORM. Bueno para consultas complejas pero sin type safety sin herramientas extra.
Opción incorrecta: Exponer consultas de base de datos a través de API routes y fetching desde Client Components. Añade latencia, complejidad, y superficie de ataque sin beneficio cuando RSC puede consultar directamente.
Por qué la mejor es la mejor: Prisma en Server Components significa consultas type-safe con cero exposición de cliente, pooling de conexión automático, y migraciones built-in.
Escenario: Una página de listado de productos tiene filtros (categoría, rango de precio, orden) que los usuarios quieren compartir y marcar.
Rango
Opción
Enfoque
Mejor
Parámetros de búsqueda (useSearchParams + Server Component)
Lee parámetros en un Server Component: searchParams.category. Filtra en el servidor, devuelve solo productos coincidentes.
2da
Biblioteca nuqs
Gestión de estado URL type-safe con useQueryState. Maneja serialización y valores por defecto elegantemente.
3ra
useState del lado del cliente
Actualizaciones rápidas de UI pero filtros se pierden en recarga, no pueden ser compartidos, y daña SEO.
Opción incorrecta: Almacenar filtros en useState solo. Los usuarios no pueden compartir vistas filtradas, el botón atrás no funciona, y los motores de búsqueda no pueden indexar páginas filtradas.
Por qué la mejor es la mejor: Los parámetros de búsqueda de URL hacen filtros compartibles, bookmarkables, SSR-friendly, y el servidor puede optimizar la consulta de base de datos basada en filtros.
Escenario: Hacer clic en "Editar Perfil" debe abrir un overlay modal.
Rango
Opción
Enfoque
Mejor
Ruta de intercepción + ruta paralela
Dos características de Next.js se combinan: una ruta paralela (@modal) renderiza un slot junto a la página, y una ruta de intercepción ((.)edit-profile) captura la navegación del lado del cliente y la renderiza dentro de ese slot como un modal en lugar de una página completa. Ver estructura de archivo y explicación abajo.
2da
Client Component con <dialog>
Usa elemento <dialog> nativo con useRef. Accesible, sin biblioteca necesaria.
3ra
Headless UI / Radix Dialog
Foco trap, animaciones y accesibilidad gestionadas por biblioteca. Confiable pero añade una dependencia.
Cómo funciona el patrón ruta de intercepción + ruta paralela:
app/
layout.tsx ← renderiza {children} Y {modal}
@modal/
default.tsx ← devuelve null (sin modal por defecto)
(.)edit-profile/
page.tsx ← versión modal de edit-profile
edit-profile/
page.tsx ← versión página completa de edit-profile
Ruta paralela @modal - La carpeta @modal define un "slot" nombrado. En layout.tsx lo renderizas como una prop: export default function Layout({ children, modal }) { return <>{children}{modal}</>; }. Por defecto renderiza default.tsx (que devuelve null - sin modal visible).
Ruta de intercepción (.)edit-profile - El prefijo (.) significa "intercepta esta ruta al mismo nivel." Cuando el usuario hace clic en un <Link href="/edit-profile"> (navegación soft/del lado del cliente), Next.js coincide la ruta de intercepción dentro de @modal en lugar de la página real /edit-profile. Lo renderizas como un overlay modal.
Navegación dura (URL directa, recarga) - Si alguien pega yoursite.com/edit-profile en el navegador o recarga, el interceptor no se activa. Next.js renderiza el app/edit-profile/page.tsx completo como una página regular. El slot modal permanece null.
El resultado: Hacer clic en "Editar Perfil" abre un modal (rápido, sin cambio de página). Compartir la URL da al destinatario la página completa. Recargando la URL del modal también muestra la página completa. Una URL, dos presentaciones.
Opción incorrecta: Un div con display: none/block toggled vía estado. Sin foco trapping, sin manejo de tecla escape, no accesible para lectores de pantalla, y el modal sin URL no puede ser compartido o marcado.
Por qué la mejor es la mejor: Las rutas de intercepción dan al modal una URL real -- los usuarios pueden compartirla, recargando muestra una página completa, y el modal del lado del cliente evita una navegación completa. Es el único patrón que proporciona dos presentaciones (modal vs. página) desde una única URL con cero gestión de estado extra.
Escenario: Tu app necesita una característica de búsqueda para un catálogo de productos con 50,000+ items.
Rango
Opción
Enfoque
Mejor
Búsqueda del lado del servidor vía parámetros de búsqueda
Debounce de entrada, empuja a URL: router.push(?q=term). Server Component consulta la base de datos con ILIKE o búsqueda full-text.
2da
Servicio de búsqueda dedicado
Usa Algolia, Meilisearch, o Elasticsearch vía un Server Action. Mejor relevancia y tolerancia de typos.
3ra
Filtro del lado del cliente con useMemo
Carga todos los productos y filtra del lado del cliente. Solo viable para conjuntos de datos pequeños (bajo 500 items).
Opción incorrecta: Cargar 50,000 productos en memoria del cliente y filtrar con .filter(). Cuelga navegadores móviles y desperdicia ancho de banda.
Por qué la mejor es la mejor: La búsqueda del lado del servidor aprovecha índices de base de datos, envía solo resultados coincidentes por la red, y mantiene la consulta de búsqueda en la URL para compartir.
Escenario: Un componente renderiza una lista filtrada derivada de props y pasa un manejador a componentes hijo.
Rango
Opción
Enfoque
Mejor
Usa React Compiler (React 19)
Habilita el React Compiler -- auto-memoiza. Sin useMemo/useCallback manual necesario.
2da
useMemo/useCallback orientado
Memoiza solo la computación cara y el callback pasado a hijos envueltos en memo().
3ra
Memoizar todo
Envuelve cada valor derivado y manejador. Añade overhead de memoria y complejidad para ganancias marginales.
Opción incorrecta: Nunca memoizar, incluso cuando has medido un problema de rendimiento. Si perfilado muestra re-render de 200ms desde un filtro caro, useMemo es la corrección correcta.
Por qué la mejor es la mejor: El React Compiler analiza estáticamente tu código e inserta memoización exactamente donde se necesita -- sin esfuerzo de desarrollador, sin oportunidades perdidas, sin sobre-memoización.
Escenario: Tu app necesita diferentes URLs de API para desarrollo, staging y producción.
Rango
Opción
Enfoque
Mejor
.env.local + prefijo NEXT_PUBLIC_
Variables solo-servidor en .env.local. Variables expuestas al cliente prefijadas con NEXT_PUBLIC_. Next.js maneja el resto.
2da
Variables de ambiente de plataforma
Establece variables en dashboard Vercel/AWS. Misma convención, gestionada externamente.
3ra
Archivo de config con switch de ambiente
config.ts con verificaciones process.env.NODE_ENV. Funciona pero duplica lo que archivos .env ya hacen.
Opción incorrecta: Hardcodificar URLs de API o cometer archivos .env con secretos a git. Los secretos escapan, el switch de ambiente se rompe.
Por qué la mejor es la mejor: La convención .env de Next.js es built-in, soporta sobrescrituras por-ambiente (.env.production), y el prefijo NEXT_PUBLIC_ hace el límite servidor/cliente explícito.
Escenario: Una tabla de admin muestra 10,000 registros de usuario y necesita paginación.
Rango
Opción
Enfoque
Mejor
Paginación del lado del servidor vía parámetros de búsqueda
Página y límite viven en la URL (?page=2&limit=20). El Server Component los lee, consulta la base de datos con OFFSET/LIMIT (o equivalente), y devuelve solo esa página de filas. Ver explicación y código abajo.
2da
Paginación basada en cursor
Usa ?cursor=abc123 para paginación estable en datos que cambian frecuentemente. Mejor para feeds en tiempo real.
3ra
Paginación del lado del cliente
Fetching de todos los datos, paginación en memoria. Solo funciona para conjuntos de datos pequeños.
Cómo funciona paginación del lado del servidor vía parámetros de búsqueda:
La URL es la fuente de verdad - La página actual y tamaño de página son parámetros de búsqueda (?page=2&limit=20). Esto significa el estado de paginación es compartible, bookmarkable, y sobrevive recarga. Los botones atrás/adelante navegan páginas gratis.
Server Component lee parámetros y consulta - El page.tsx recibe searchParams como prop. Calcula OFFSET y LIMIT, consulta solo ese slice de la base de datos, y también fetching el conteo total para controles de página.
Client Component maneja navegación - Una pequeña barra de paginación "use client" usa useRouter o <Link> para actualizar parámetros de búsqueda. Sin lógica de fetching de datos en el cliente.
Streaming funciona automáticamente - Porque el fetch de datos está en un Server Component, envolviéndolo en <Suspense> muestra un esqueleto de carga mientras la consulta se ejecuta. Navegar entre páginas fluye el nuevo contenido.
Puntos clave: La base de datos solo devuelve 20 filas por solicitud (no 10,000). La URL te dice exactamente qué página estás en. Cambiar páginas es una navegación del servidor -- sin código de fetching de datos del lado del cliente. <Link> con parámetros de búsqueda te da prefetching y streaming gratis.
Opción incorrecta: Scroll infinito que fetching todos los 10,000 registros en memoria. El navegador se cuelga, pesadilla de accesibilidad, y los usuarios no pueden saltar a página 50.
Por qué la mejor es la mejor: La paginación del lado del servidor pone estado de página en la URL (compartible, bookmarkable), fetching solo el slice necesario de la base de datos, fluye HTML vía RSC, y requiere cero lógica de fetching de datos del lado del cliente.
Escenario: Un formulario necesita crear un nuevo registro en la base de datos.
Rango
Opción
Enfoque
Mejor
Server Action
Función "use server" llamada desde <form action={...}>. Type-safe, mejorada progresivamente, sin endpoint manual.
2da
API Route (Route Handler)
app/api/records/route.ts con un manejador POST. Mejor cuando clientes externos (apps móviles, webhooks) necesitan el endpoint.
3ra
tRPC
Type-safety end-to-end con contrato cliente/servidor. Poderoso pero pesado si Server Actions cubren tus necesidades.
Opción incorrecta: Crear rutas API solo para mutaciones usadas por tu propio frontend Next.js. Server Actions eliminan el boilerplate de gestión de endpoints, serialización y manejo de errores.
Por qué la mejor es la mejor: Los Server Actions están co-localizados con tu UI, automáticamente manejan serialización, soportan mejora progresiva, e integran con useActionState para estados pendiente/error.
Escenario: Tu app tiene 30+ rutas entre múltiples áreas de características (auth, dashboard, settings, marketing público).
Rango
Opción
Enfoque
Mejor
Grupos de ruta + co-localización de características
app/(auth)/login, app/(dashboard)/analytics, app/(marketing)/pricing. Layouts compartidos por grupo.
2da
Carpetas de características fuera de app/
src/features/auth/, src/features/dashboard/ con componentes, hooks y utils. app/ solo tiene archivos de ruta delgados.
3ra
Estructura app/ plana
Todas las rutas en nivel superior. Funciona para apps pequeñas pero se vuelve inmanejable en 30+ rutas.
Opción incorrecta: Organizar por tipo de archivo (components/, hooks/, utils/) en lugar de característica. Los desarrolladores saltan entre 5 directorios para trabajar en una característica.
Por qué la mejor es la mejor: Los grupos de ruta te dejan compartir layouts y middleware por área de característica, mantener archivos relacionados juntos, y los nombres entre paréntesis no afectan la URL.
Escenario: Tu app necesita mostrar notificaciones en vivo conforme llegan.
Rango
Opción
Enfoque
Mejor
WebSockets (Socket.io o nativo)
Conexión persistente, bidireccional, baja latencia. Usa un Client Component para gestionar la conexión.
2da
Server-Sent Events (SSE)
Empuje unidireccional servidor-a-cliente. Más simple que WebSockets, auto-reconexión, funciona a través de proxies.
3ra
Polling con setInterval
Fetching cada N segundos. Simple pero desperdicia ancho de banda y tiene latencia inherente.
Opción incorrecta: Polling cada segundo con useEffect + fetch. Golpea el servidor, drena baterías móviles, y aún tiene hasta 1 segundo de latencia.
Por qué la mejor es la mejor: WebSockets entregan mensajes instantáneamente con una única conexión persistente, soportando envío y recepción sin overhead HTTP repetido.
Escenario: Un componente <CheckoutForm> maneja validación, envío y visualización de errores.
Rango
Opción
Enfoque
Mejor
React Testing Library + Vitest
Prueba comportamiento del usuario: rellena campos, envía, afirma que aparecen mensajes de error. render(<CheckoutForm />).
2da
Playwright/Cypress E2E
Prueba el flujo completo en un navegador real. Más lento pero captura problemas de integración en la pila.
3ra
Pruebas de snapshot
expect(tree).toMatchSnapshot(). Captura cambios inesperados pero no verifica comportamiento.
Opción incorrecta: Pruebar detalles de implementación (estado interno, llamadas de método, clases CSS). Las pruebas se rompen en cada refactor incluso cuando el comportamiento es sin cambios.
Por qué la mejor es la mejor: Las pruebas RTL prueban lo que los usuarios ven y hacen -- si la prueba pasa, el componente funciona. Refactorizar internals no rompe pruebas, así que actualmente mantienen confianza.
Escenario: Cada página de producto necesita título, descripción y tags Open Graph únicos.
Rango
Opción
Enfoque
Mejor
Función generateMetadata
Exporta async function generateMetadata({ params }) que fetching datos de producto y devuelve { title, description, openGraph }.
2da
Export metadata estático
export const metadata = { title: '...' }. Bueno para páginas con metadatos fijos.
3ra
<Head> desde next/head
Solo Pages Router. Del lado del cliente, no funciona con RSC.
Opción incorrecta: No establecer metadatos en absoluto o usar el mismo título en cada página. Los motores de búsqueda no pueden diferenciar tus páginas, y los compartidos sociales se ven rotos.
Por qué la mejor es la mejor:generateMetadata se ejecuta en el servidor, puede fetching datos para generar títulos dinámicos, y se deduplica con el fetch de datos de la página vía caché fetch.
Escenario: Los usuarios cargan avatares de perfil (máx 5MB) a tu app.
Rango
Opción
Enfoque
Mejor
Carga de URL pre-firmada a S3/R2
Server Action genera una URL pre-firmada. Cliente carga directamente a almacenamiento. Sin presión de memoria del servidor.
2da
Server Action con FormData
const file = formData.get('avatar'). Servidor recibe el archivo y lo reenvía a almacenamiento. Más simple pero limpio por memoria.
3ra
API Route con análisis estilo multer
Enfoque clásico. Más control pero más código y configuración.
Opción incorrecta: Almacenar archivos cargados en el sistema de archivos del servidor Next.js. Contenedores efímeros (Vercel, Docker) pierden archivos en redespliegue. Los archivos deben ir a almacenamiento duradero.
Por qué la mejor es la mejor: Las URLs pre-firmadas permiten al cliente cargar directamente a S3/R2 -- el servidor nunca toca los bytes, así que maneja cualquier tamaño de archivo sin presión de memoria.
Escenario: Tu sitio de marketing necesita soportar inglés, español y japonés.
Rango
Opción
Enfoque
Mejor
next-intl con App Router
Middleware detecta locale, grupos de ruta por idioma [locale]/, traducciones del lado del servidor.
2da
next-i18next (Pages Router)
Maduro, bien documentado. Mejor opción si aún estás en Pages Router.
3ra
Enrutamiento locale manual
Construye tu propio segmento dinámico [locale] y cargador de traducción. Control total pero reinventa la rueda.
Opción incorrecta: Traducción solo del lado del cliente (cargando bundles JSON en useEffect). El contenido brilla en el idioma predeterminado antes de cambiar, y los motores de búsqueda solo ven el predeterminado.
Por qué la mejor es la mejor:next-intl se integra con middleware y Server Components del App Router, así que el contenido traducido está en el HTML inicial -- sin brillo, SEO completo, y detección locale automática.
Escenario: Tu equipo está listo para desplegar una app Next.js de producción.
Rango
Opción
Enfoque
Mejor
Vercel
Despliegue cero-config, funciones edge, soporte ISR, análisis, despliegues de vista previa por PR.
2da
Autoalojado con next start
Ejecuta el servidor de producción Next.js en cualquier máquina que controles. Control total de infraestructura, pero gestionas todo tú mismo. Ver explicación abajo.
3ra
Contenedor Docker
Construye una imagen Docker con salida standalone. Bueno para Kubernetes o infraestructura de contenedor existente.
Cómo funciona autoalojamiento con next start:
Next.js incluye un servidor de producción Node.js built-in. Construyes la app una vez, luego la ejecutas como un proceso de vida larga en cualquier servidor con Node.js instalado.
Paso de construcción - Ejecuta next build en tu CI o en el servidor. Esto compila todas las páginas, genera activos estáticos, y produce el directorio de salida .next/. Los Server Components se pre-renderizan, bundles de cliente están optimizados y divididos por código.
Paso de inicio - Ejecuta next start (predeterminado a puerto 3000). Esto lanza un servidor HTTP Node.js que maneja enrutamiento, SSR, Server Actions, revalidación ISR y middleware -- todas las características que obtienes en Vercel, solo ejecutándose en tu máquina.
Lo que necesitas gestionar tú mismo:
Inquietud
Lo que Vercel hace por ti
Lo que haces autoalojado
Gestión de procesos
Automático
Usa PM2 o systemd para mantener el proceso vivo y reiniciar en fallo
HTTPS / TLS
Automático
Pon Nginx o Caddy en frente como proxy inverso con terminación TLS
Escalado
Auto-escalas
Ejecuta múltiples instancias detrás de un balanceador de carga (ALB, Nginx upstream)
CDN / activos estáticos
CDN edge built-in
Configura CloudFront, Cloudflare, o sirve desde Nginx
Caché ISR
Caché distribuida gestionada
Funciona de la caja en un servidor único; para multi-servidor, configura un manejador de caché compartido
Variables de ambiente
UI de dashboard
Establece en .env.production, archivo de unidad systemd, o tu herramienta de despliegue
Despliegues cero-tiempo de inactividad
Automático
Despliegues azul-verde o rodantes vía tus propios scripts
# Despliegue autoalojado típico en una instancia EC2/DigitalOcean# 1. Construir (a menudo hecho en CI, luego artefactos se copian al servidor)npm cinext build# 2. Inicio con PM2 para gestión de procesospm2 start npm --name "my-app" -- start# o directamente:pm2 start node_modules/.bin/next --name "my-app" -- start -p 3000# 3. Proxy inverso Nginx (simplificado)# /etc/nginx/sites-available/my-app# server {# listen 443 ssl;# server_name myapp.com;# location / {# proxy_pass http://localhost:3000;# proxy_http_version 1.1;# proxy_set_header Upgrade $http_upgrade;# proxy_set_header Connection 'upgrade';# proxy_set_header Host $host;# proxy_cache_bypass $http_upgrade;# }# }
Cuándo elegir autoalojado sobre Vercel: Necesitas permanecer dentro de un proveedor de nube específico (p. ej., todo en AWS VPC), la política de tu empresa prohíbe hosting de terceros, necesitas middleware de servidor personalizado (p. ej., actualizaciones de WebSocket), o el costo es una inquietud en volúmenes de tráfico alto donde el precio de Vercel excede un servidor dedicado.
Opción incorrecta: Exportar como HTML estático (next export / output: 'export') cuando tu app usa Server Components, middleware o ISR. Estas características requieren un runtime Node.js -- la exportación estática silenciosamente las abandona.
Por qué la mejor es la mejor: Vercel está construido por el equipo Next.js -- características como ISR, middleware y Server Actions funcionan de la caja sin configuración de infraestructura cero. El autoalojamiento es la mejor opción 2da cuando necesitas ese control de infraestructura, pero espera ser dueño de la carga de operaciones.
Revisado por Chris St. John·Última actualización: 10 jul 2026