Next.js vs Express: solo uno de los dos renderiza
Express es un servidor sobre el que construyes. Next.js es un framework de renderizado con un servidor incorporado. Elegir entre ambos es elegir cuánto quieres escribir tú.
A estos dos se los compara porque los dos son Node y los dos sirven HTTP, que es más o menos donde acaba el parecido. Express es un framework HTTP mínimo que compones tú. Next.js es un framework de renderizado que además incluye un servidor.
La decisión no es realmente una comparación. Es una pregunta sobre qué estás construyendo.
| Express | Next.js | |
|---|---|---|
| Qué te da | Una tubería de peticiones | Un camino de renderizado |
| Enrutado | El que escribas | Sistema de archivos, con layouts |
| Renderizado | Ninguno | Todo su propósito |
| Empaquetado del cliente | No es su trabajo | Incorporado |
| Conexiones de larga vida | Nativas | De incómodo a imposible |
| Qué montas tú | Casi todo un front end | Casi todo un backend |
Si estás construyendo una API y nada más
Usa Express, o Fastify, o Hono. Los route handlers de Next.js existen y funcionan, pero estarías asumiendo un framework de renderizado, un paso de build, un enrutador basado en sistema de archivos y una dependencia de React para un servicio que no renderiza nada.
En concreto, Express sigue siendo mejor cuando tu API tiene:
- Consumidores externos. Rutas versionadas, claves de API, contratos documentados.
- Su propia cadena de middleware. Límite de tasa, validación de peticiones, autenticación a medida, logging que es tuyo.
- Algo de larga duración. WebSockets, server-sent events, conexiones que sobreviven a una petición.
- Restricciones de despliegue. Un proceso Node simple es más fácil de operar sobre infraestructura con opiniones.
El punto del middleware merece más que una viñeta, porque es el que se
descubre tarde. Una aplicación Express es una tubería que montas: cada petición
pasa por las funciones que registraste, en el orden en que las registraste, y
puedes razonar sobre toda la cadena leyendo un archivo. Next.js no tiene nada
equivalente a nivel de aplicación. proxy.ts - la convención que sustituyó a
middleware en la 16 - se ejecuta sobre un matcher, en peticiones que quizá no
pretendías, y no es una tubería de peticiones.
Intentar usarla como tal es la forma más común en que un equipo traslada una
costumbre de Express a una base de código que no puede sostenerla.
Si renderizas páginas, no lo construyas tú
La dirección contraria es más contundente y menos obvia.
Los equipos que empiezan con Express y añaden React encima acaban escribiendo a mano lo que Next.js ya decidió: enrutado, code splitting, obtención de datos en el servidor, streaming, la frontera del bundle de cliente, manejo de imágenes, caché. Cada uno es una semana. Juntos son un framework, y será uno peor, porque es un proyecto paralelo y no el producto.
Si tu aplicación tiene páginas, el argumento a favor de Express es el argumento a favor de escribir tu propio framework. Eso es correcto de vez en cuando y normalmente no.
Lo que se subestima no es ninguno de los puntos de esa lista por separado. Es que esas decisiones interactúan entre sí. El code splitting tiene que saber del enrutado. El streaming tiene que saber de la capa de datos. La frontera de cliente tiene que saber de ambos. Un framework vale lo que vale porque alguien hizo que esas decisiones fueran consistentes entre sí, y un stack propio es donde poco a poco dejan de serlo.
Dónde los route handlers bastan de verdad
Para una API que solo consume tu propio front end, los route handlers de Next.js hacen el trabajo:
// app/api/products/route.ts
export async function GET(request: Request) {
const products = await db.product.findMany();
return Response.json(products);
}Un despliegue, una base de código, tipos compartidos entre la API y los componentes que la llaman. Para la mayoría de las aplicaciones de producto, esta es la cantidad correcta de maquinaria.
Conviene saberlo: para los datos que necesitan tus propias páginas, a menudo no hace falta una ruta. Un Server Component puede consultar directamente, y una Server Action puede mutar; el rodeo por tu propia API HTTP es una costumbre heredada de una arquitectura renderizada en el cliente.
Dónde los route handlers dejan de bastar
Tres límites, en el orden en que los equipos los encuentran.
La conexión tiene que quedarse abierta. Un route handler está construido alrededor de una petición que termina. Los server-sent events son posibles con una respuesta en streaming; un WebSocket no, y en un destino serverless ninguno de los dos sobrevive a la vida de la función. Cualquier cosa con conexión persistente pertenece a un proceso que controles tú.
Algo tiene que ocurrir según un horario. No hay ejecutor de trabajos. Un servicio de cron golpeando un endpoint autenticado cubre los casos simples, y deja de cubrirlos en cuanto un trabajo necesita política de reintentos, cola de mensajes fallidos, o más tiempo del que permite el límite de ejecución de la plataforma.
La API tiene consumidores que no eres tú. Versionado, deprecación, límites de tasa por cliente, una autenticación separada de la sesión de tu front end, y documentación que funcione como contrato. Todo eso se puede construir en route handlers, y todo eso ya existe en el ecosistema que habrías elegido si no.
El arreglo en el que acaban la mayoría de los equipos
Express - o Fastify, o un servicio en Go, o Laravel - sosteniendo la lógica de negocio y los trabajos. Next.js sosteniendo el front end. Hablan por HTTP.
Es la misma conclusión que en Next.js frente a Laravel, por la misma razón: el render path y la lógica de negocio son problemas distintos, y los frameworks son buenos en uno de ellos cada uno.
El coste es real y conviene decirlo: dos destinos de despliegue, un contrato entre ellos que alguien mantiene, y tipos que hay que mantener sincronizados a través de una frontera de red en lugar de comprobados por un único compilador. Los equipos que adoptan esta división sin reconocer ese coste acaban con un front end que reimplementa la mitad de las reglas de la API para evitarse un viaje de ida y vuelta.
Lo que no hay que hacer
Un servidor Express personalizado delante de Next.js. Está soportado y desactiva la optimización estática automática en las rutas que sirve, que es casi todo aquello por lo que viniste. Si necesitas un proxy, ponlo delante de los dos procesos en lugar de dentro de uno.
Portar una cadena de middleware de Express a proxy.ts. La convención que
sustituyó a middleware en la 16 corre en Node,
no es configurable y se ejecuta en peticiones que
no tenías previstas. Es para decisiones de enrutado, no para un pipeline de
peticiones.
