Inicio / Artículos / Migrar las API de Express a los controladores de rutas del App Router de Next.js

Este artículo está publicado en inglés.

Next.jsExpressReactComponentes del servidorMigraciónNode.js

Migrar las API de Express a los controladores de rutas del App Router de Next.js

Aprenda cómo convertir rutas Express, middleware y patrones de datos a Next.js App Router con componentes server-side y las consideraciones de despliegue.

3059 palabras

Cuándo tiene sentido migrar a Next.js (y cuándo no)

El App Router de Next.js se convierte en una opción razonable cuando tu proyecto cumple con al menos dos de estas condiciones:

  • La velocidad de carga inicial o la visibilidad en los motores de búsqueda es crítica y actualmente utilizas una aplicación de página única que obtiene datos desde un backend Express después de que se carga la página.
  • Tu pipeline de despliegue ya está orientado a Vercel o plataformas similares o estás dispuesto a adoptar esa infraestructura; las características de edge y serverless del App Router presuponen este entorno de hosting.
  • Tu aplicación Express sirve principalmente páginas y gestiona operaciones CRUD estándar en lugar de administrar conexiones persistentes, tareas en segundo plano o procesos intensivos en cálculos.
  • Su equipo de ingeniería se compromete a comprender a fondo los Server Components. Este framework no simplemente agrega enrutamiento basado en archivos a React. La arquitectura de renderizado difiere fundamentalmente, y aplicar patrones antiguos de aplicaciones de página única generará una aplicación con un rendimiento peor y que confundirá aún más a los desarrolladores que la original.
  • La migración no es recomendable cuando:

    • Su backend realiza tareas significativas además del renderizado de páginas: colas de mensajes, tareas programadas, puntos finales gRPC o conexiones de socket persistentes. Los controladores de ruta en Next.js no pueden reemplazar un servicio backend dedicado; probablemente desplegará Next.js como capa frontend mientras mantiene un servicio separado de Express o Node en ejecución detrás de él.
    • Depende en gran medida de un ecosistema establecido de middleware de Express: proveedores especializados de autenticación, bibliotecas de limitación de tasa o integraciones de observabilidad, que requerirían una reimplementación completa para obtener beneficios mínimos.
    • Su aplicación consta principalmente de interfaces de panel de control interactivas y autenticadas con requisitos limitados en cuanto a motores de búsqueda; las principales ventajas del App Router (renderizado en servidor en tiempo real, optimización de búsquedas y generación estática) ofrecen poco valor en este escenario, mientras que la inversión en capacitación sigue siendo considerable.

    Un enfoque recomendado: mantenga su API Express como fuente autoritativa de datos y reglas de negocio, y utilice Next.js como capa de renderizado y backend para el frontend. Migre primero la interfaz de usuario y las rutas orientadas a lectura, dejando sin cambios los servicios internos con muchos operaciones de escritura. Esta guía sigue esa estrategia de migración.

    Asociación de rutas Express con los controladores de rutas de Next.js

    Los controladores de rutas ubicados en app/api/**/route.ts ofrecen el reemplazo más directo para las definiciones de rutas de Express. El cambio conceptual: en lugar de los objetos req y res, se recibe un objeto Request y se devuelve uno Response (o se puede utilizar NextResponse para mayor comodidad), y cada método HTTP se convierte en una función exportada separada en lugar de una llamada al método router.get().

    Considere una ruta estándar de Express que recupera y crea pedidos:

    // express: routes/orders.ts
    import ⟨0⟩ from "express";
    import ⟨1⟩ from "../db";
    import ⟨2⟩ from "../middleware/auth";
    
    const router = Router(); router.get("/api/orders", requireAuth, async (req, res) => {
     const userId = req.user.id;
     const orders = await db.order.findMany({ where: ⟨3⟩ });
     res.json({ orders });
    }); router.post("/api/orders", requireAuth, async (req, res) => {
     const ⟨5⟩ = req.body;
     if (!items?.length) {
     return res.status(400).json({ error: "items required" });
     }
     const order = await db.order.create({
     data: ⟨7⟩,
     });
     res.status(201).json({ order });
    }); export default router;
    

    La implementación correspondiente del manejador de rutas:

    // app/api/orders/route.ts
    import { NextRequest, NextResponse } from "next/server";
    import { db } from "@/lib/db";
    import { getSessionUser } from "@/lib/auth";
    
    export async function GET(req: NextRequest) {
     const user = await getSessionUser(req);
     if (!user) {
     return NextResponse.json({ error: "unauthorized" }, { status: 401 });
     } const orders = await db.order.findMany({ where: { userId: user.id } });
     return NextResponse.json({ orders });
    }export async function POST(req: NextRequest) {
     const user = await getSessionUser(req);
     if (!user) {
     return NextResponse.json({ error: "unauthorized" }, { status: 401 });
     } const body = await req.json();
     if (!body.items?.length) {
     return NextResponse.json({ error: "items required" }, { status: 4
    00 }); } const order = await db.order.create({ data: { userId: user.id, items: body.items }, }); return NextResponse.json({ order }, { status: 201 }); }

    Varios detalles merecen atención al traducir patrones de Express a controladores de rutas:

    Segmentos dinámicos utilizan una sintaxis basada en carpetas en lugar de prefijos con dos puntos. Una ruta de Express en /api/orders/:id se convierte en app/api/orders/[id]/route.ts. El parámetro llega como el segundo argumento de la función del controlador: GET(req, { params }: { params: Promise<{ id: string }> }). Las versiones actuales de Next.js entregan los parámetros como una promesa, por lo que es necesario esperarla antes de leer su valor.

    No existe una cadena de middleware por ruta. El middleware requireAuth de Express se transforma en una función utilitaria compartida que se invoca al inicio de cada manejador (como se muestra en el ejemplo anterior), o preferiblemente en lógica dentro de middleware.ts (que se analiza en una sección posterior), de modo que los manejadores individuales de las rutas no tengan conocimiento de los aspectos relacionados con la autenticación.

    El análisis del cuerpo requiere llamadas explícitas: debes escribir await req.json() en lugar de depender de express.json(). No hay un análisis automático, lo cual en realidad mejora la claridad: evitas los límites inesperados de tamaño del cuerpo impuestos por un middleware global que configuraste hace meses y olvidaste.

    Los manejadores de rutas siguen siendo funciones normales de Node o Edge. Si validaste la entrada con zod en tus rutas de Express, ese código de validación se transfiere sin modificaciones.

    Componentes del servidor frente a tus patrones existentes de React renderizado en el cliente

    Este aspecto sorprende a los equipos más que cualquier otro. En una arquitectura de Express con React, cada componente es por defecto un Componente del Cliente: se renderiza en el navegador y, cuando necesita datos, llama a tu API desde useEffect o mediante una biblioteca de obtención de datos como React Query.

    // Patrón antiguo: React renderizado en el cliente que se comunica con Express
    function OrderList() {
     const [orders, setOrders] = useState<Order[] | null>(null);
    
     useEffect(() => {
     fetch("/api/orders", { credentials: "include" })
     .then((r) => r.json())
     .then((data) => setOrders(data.orders));
     }, []); if (!orders) return <Spinner />;
     return (
     <ul>
     {orders.map((o) => (
     <li key={o.id}>{o.id} — ${o.total}</li>
     ))}
     </ul>
     );
    }
    

    El App Router invierte esta configuración por defecto. Cada componente es un componente del servidor a menos que se especifique lo contrario, lo que significa que se ejecuta en el servidor, accede directamente a su base de datos o servicios y nunca envía su JavaScript al cliente. Puede omitir por completo la ruta API para los datos que pertenecen a la página en cuestión.

    Consulte directamente:

    // app/orders/page.tsx — Componente servidor, sin “use client”
    import { db } from "@/lib/db";
    import { getSessionUser } from "@/lib/auth";
    import { redirect } from "next/navigation";
    
    export default async function OrdersPage() {
     const user = await getSessionUser();
     if (!user) redirect("/login"); // acceso directo a la BD, sin fetch, sin estado de carga, sin costo adicional del paquete cliente
     const orders = await db.order.findMany({
     where: { userId: user.id }
    rver proporciona HTML que ya contiene los datos. Solo agrega "use client" cuando un componente requiere interactividad: estado, efectos, manejadores de eventos o APIs exclusivas del navegador:

    // app/orders/OrderFilters.tsx
    "use client";
    
    import > from "react";
    import > from "next/navigation"; export function OrderFilters() {
     const router = useRouter();
     const params = useSearchParams();
     const [status, setStatus] = useState(params.get("status") ?? "all"); function apply(next: string) {
     setStatus(next);
     const url = new URLSearchParams(params);
     url.set("status", next);
     router.push(`/orders?$>`);
     } return (
     <select value=> onChange=>>
     <option value="all">Todos</option>
     <option value="pending">Pendiente</option>
     <option value="shipped">Enviado</option>
     </select>
     );
    }
    
    /code>

    La guía práctica para los equipos: introduzca “use client” lo más profundo posible en el árbol de componentes. Mantenga sus páginas y diseños como Componentes del Servidor; aplique la directiva únicamente a los componentes hoja que realmente necesiten interactividad. Si, por costumbre, agrega “use client” a cada componente (lo cual ocurre al migrar una aplicación de página única sin replantear la arquitectura), no obtendrá ninguna ventaja del App Router y terminará con un modelo mental peor que el de su configuración original.

    Sustituyendo el middleware de autenticación de Express por el middleware de Next.js

    En Express, el middleware de autenticación se ejecuta para cada ruta dentro del proceso Node.js. Next.js proporciona middleware.ts, que intercepta las solicitudes en la capa de borde antes de que lleguen a cualquier página o manejador, funcionando como el equivalente arquitectónico:

    // Antiguo: middleware/auth.ts (Express)
    import jwt from "jsonwebtoken";
    
    export function requireAuth(req, res, next) {
     const token = req.cookies.session;
     if (!token) return res.status(401).json([0]); try {
     req.user = jwt.verify(token, process.env.JWT_SECRET!);
     next();
     } catch {
     res.status(401).json([1]);
     }
    }
    
    // middleware.ts: se encuentra en la raíz del proyecto
    import { Server } from "next/server";
    import { jwt } from "jose"; // compatible con Edge, a diferencia de jsonwebtoken
    
    const PROTECTED_PREFIXES = [/dashboard/, /orders/, /api/orders/]; export async function
    n middleware(req: NextRequest) { const isProtected = PROTECTED_PREFIXES.some((p) => req.nextUrl.pathname.startsWith(p) ); if (!isProtected) return NextResponse.next(); const token = req.cookies.get("session")?.value; if (!token) { return NextResponse.redirect(new URL("/login", req.url)); } try { const secret = new TextEncoder().encode(process.env.JWT_SECRET!); const [0] = await jwtVerify(token, secret); // enviar el ID de usuario verificado hacia abajo a través de un encabezado de solicitud const headers = new Headers(req.headers); headers.set("x-user-id", String(payload.sub)); return NextResponse.next({ request: [1] }); } catch { return NextResponse.redirect(new URL("/login", req.url)); } }export const config = { matcher: [/"/dashboard/:path*"/, "/"/orders/:path*"/, "/"/api/orders/:path*"/], };

    Dos

    Problemas que obstaculizan de forma constante a los equipos en proceso de migración:

    1. Por defecto, el middleware se ejecuta en el entorno Edge, no en Node.js. Cualquier biblioteca que dependa de módulos principales de Node.js—jsonwebtoken, clientes de bases de datos típicos—fallará o funcionará incorrectamente en silencio. Adopte alternativas preparadas para el entorno Edge: jose es la opción habitual para operaciones con JWT. Transfiera todas las consultas a bases de datos a Componentes del Servidor o Controladores de Ruta, donde está presente el entorno completo de Node.js; nunca intente realizarlas en el middleware.
  • Las consultas completas a la base de datos durante una sesión son poco prácticas en el middleware, a diferencia de los patrones de Express que pueden ejecutar SELECT * FROM sessions WHERE id = ?. Limiten la lógica del middleware a operaciones rápidas y sin estado, como la verificación de firmas de tokens. Dejen las cuestiones de autorización —“¿puede este usuario ver este pedido?”— para el componente de la página o el propio manejador, donde el acceso a la base de datos y las APIs de Node.js están completamente disponibles.
  • Patrones de obtención de datos: Componentes del servidor vs. su enfoque actual de llamadas a API

    La mayoría de las arquitecturas Express con React siguen una secuencia predecible: el componente se monta, hace una solicitud a su API, la API consulta la base de datos, el JSON vuelve al navegador y el componente se actualiza. Dos saltos en la red —desde el cliente al servidor, y desde el servidor a la base de datos— entregan información que el servidor ya tenía.

    Con App Router, las operaciones de lectura en los Componentes del Servidor comprimen este proceso en un solo paso: el servidor consulta la base de datos y transmite directamente al cliente HTML que contiene el resultado, como se muestra en el código anterior de OrdersPage. En cuanto a las operaciones de escritura, existen dos patrones habituales: los Controladores de Ruta cuando se necesita una API convencional (por ejemplo, una interfaz REST pública) o las Acciones del Servidor para las mutaciones desencadenadas por los propios formularios y la interfaz de usuario.

    Las Acciones del Servidor difieren más significativamente de las convenciones de Express. Se define una función que se ejecuta en el servidor y se invoca directamente desde un formulario, sin necesidad de crear ninguna ruta API explícita:

    // app/orders/actions.ts
    "use server";
    
    import { db } from "@/lib/db";
    import { getSessionUser } from "@/lib/auth";
    import { revalidatePath } from "next/cache";
    import { redirect } from "next/navigation";export async function createOrder(formData: FormData) {
     const user = await getSessionUser();
     if (!user) redirect("/login"); const itemId = formData.get("itemId");
     if (typeof itemId !== "string" || !itemId) {
     throw new Error("Se requiere itemId");
     } await db.order.create({
     data: { userId: user.id, items: [{ itemId, qty: 1 }] },
     }); // Volver a renderizar la página de pedidos con datos recientes del servidor; no es necesario volver a obtenerlos desde el cliente
     revalidatePath("/orders");
    }
    
    // app/or
    ders/NewOrderForm.tsx import { createOrder } from "./actions";
    export function NewOrderForm() {
     return (
     <form action={createOrder}>
     <input type="text" name="itemId" placeholder="ID del artículo" required />
     <button type="submit">Crear pedido</button>
     </form>
     );
    }
    

    Elegir entre acciones del servidor y controladores de ruta:

    Una acción del servidor es una llamada a función interna desde tu interfaz de usuario hacia el servidor; un controlador de ruta es un endpoint HTTP adecuado. La distinción es importante al decidir qué patrón se ajusta a una mutación determinada.

    Utilice un Route Handler cuando el endpoint debe ser estable y accesible desde fuera de esta aplicación Next.js: un cliente móvil, una integración de terceros o una API pública. Los Route Handlers le proporcionan rutas URL explícitas, verbos HTTP y un contrato que usted controla y versiona.

    Utilice una Server Action cuando la mutación proviene de sus propios formularios y componentes interactivos. Las Server Actions requieren menos código genérico y vuelven a validar automáticamente los datos en caché mediante revalidatePath o revalidateTag, eliminando la necesidad de realizar manualmente operaciones para invalidar el caché después de una llamada fetch. No están diseñadas para consumidores externos ni ofrecen garantías de compatibilidad retroactiva; trátelas como llamadas a procedimientos remotos internos.

    La prueba práctica: si desea documentar el endpoint para alguien fuera de su equipo, conviértalo en un Route Handler. Si existe únicamente para respaldar un botón o formulario en su interfaz de usuario, una Server Action es más sencilla.

    Diferencias en el despliegue (Vercel vs ECS/EB)

    Los equipos acostumbrados a desplegar Express en ECS o Elastic Beanstalk se encuentran con un cambio arquitectónico fundamental al usar Next.js en Vercel. Una aplicación Express funciona como un proceso Node de larga duración: un solo proceso maneja muchas solicitudes, mantiene conexiones activas con la base de datos y presenta características predecibles en cuanto a memoria y tiempo de inicio.

    Next.js en Vercel despliega las páginas y los gestores de rutas como funciones sin servidor o de borde independientes. Cada función se inicia en frío de forma independiente, opera bajo sus propios límites de ejecución y no comparte un pool de conexiones a la base de datos persistente como lo hace un único proceso Express. Si instancias un cliente Prisma de la misma manera que lo hiciste en Express, agotarás tu límite de conexiones a la base de datos bajo carga, ya que cada llamada a la función puede crear su propia conexión.

    // lib/db.ts — patrón requerido para Prisma sin servidor
    import { PrismaClient } from "@prisma/client";
    
    const globalForPrisma = global as unknown as { prisma: PrismaClient };// reutilizar el cliente en las llamadas posteriores en lugar de crear uno nuevo cada vez
    export const db =
     globalForPrisma.prisma ??
     new PrismaClient({
     // utilizar una cadena de conexión compartida (por ejemplo, PgBouncer / Prisma Accelerate / RDS Proxy)
     datasources: { db: { url: process.env.DATABASE_URL_POOLED } },
     });if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
    

    Si despliega Next.js en ECS o Elastic Beanstalk en lugar de Vercel, el framework funciona como un servidor Node tradicional. Ejecute next build seguido de next start, o configure el modo de salida independiente para generar artefactos Docker más ligeros. Mantendrá las ventajas de los procesos que permanecen activos durante mucho tiempo, pero renunciará a la red de edge automática de Vercel, a la regeneración estática incremental en el CDN y a los despliegues de vista previa sin configuración. Deberá configurar esas funciones por sí mismo o aceptar su ausencia. Se trata de una elección técnica válida, no de un compromiso; muchos equipos ejecutan Next.js en ECS precisamente porque ya poseen la infraestructura y prefieren no dividir el alojamiento entre dos proveedores.

    Dedique tiempo a dos tareas adicionales de migración: variables de entorno (Next.js requiere el prefijo NEXT_PUBLIC_ para cualquier variable expuesta al navegador; audite cada variable que haya pasado a su proceso de compilación de React) y configuración en tiempo de compilación versus en tiempo de ejecución (los valores integrados en una exportación estática se comportan de manera diferente a los valores leídos al momento de la solicitud, a diferencia de un proceso Express único donde todo ocurre en tiempo de ejecución).

    Lista de verificación para la migración y errores comunes

    Siga estos pasos en secuencia:

    1. Despliegue la aplicación Next.js en paralelo con su servidor Express; deje la aplicación existente sin modificar durante la configuración inicial.
    2. Convierta primero las páginas con mucho tráfico de lectura y optimizadas para motores de búsqueda; estas páginas se benefician más de los Componentes del Servidor y presentan un riesgo mínimo de migración.
  • Implemente la autenticación en middleware.ts con una biblioteca de verificación de tokens compatible con el entorno de ejecución en los bordes.
  • Transforme los puntos finales GET en Componentes de Servidor que obtengan los datos directamente cuando dichos datos sirvan únicamente a sus propias páginas, evitando así la necesidad de Controladores de Ruta.
  • Convierta los puntos finales de mutación en Acciones de Servidor si responden exclusivamente a su propia interfaz; mantenga los Controladores de Ruta para cualquier punto final consumido por clientes externos.
  • Resuelva su enfoque de conexión a la base de datos para la ejecución sin servidor antes de migrar cualquier operación de escritura; este paso previene interrupciones en producción, no es un detalle que pueda posponerse.
  • Ajuste los pipelines de despliegue e integración continua por último, después de que la aplicación funcione correctamente tanto en el entorno de desarrollo como en una versión de producción (next build && next start); next dev oculta ciertos errores que solo aparecen en modo producción, como las violaciones de los límites entre servidor y cliente.
  • Errores comunes durante la migración:

    • Importar código exclusivo del servidor a un componente de cliente. Cuando un componente con "use client" importa elementos que acceden a fs, al cliente de base de datos o a información confidencial, la compilación falla o, lo que es más peligroso, incluye esa información confidencial en el JavaScript del cliente. Instale el paquete server-only para que se genere un error de compilación en lugar de filtrar silenciosamente las credenciales.
  • Omitir revalidatePath o revalidateTag después de una acción del servidor. Sin una revalidación explícita, la interfaz muestra datos desactualizados tras una mutación, ya que los componentes del servidor pueden estar en caché.
  • Marcar cada componente con "use client". Las costumbres heredadas de las aplicaciones de página única son la razón principal por la que una aplicación Next.js no supera el rendimiento de la original.
  • Esperar que el middleware soporte toda la superficie de la API de Node.js. El middleware se ejecuta por defecto en el entorno Edge; diseñe sus verificaciones de autenticación dentro de las limitaciones de dicho entorno.
  • Saltar las pruebas de carga del pool de conexiones a la base de datos antes del despliegue en producción. Este descuido es el que provoca alertas en medio de la noche.
  • Conclusión

    Si sigue sin estar seguro, comience con un experimento limitado: elija una página que requiera mucho procesamiento de texto y sea crítica para el SEO en su aplicación actual, reconstrúyala como un Componente de Servidor que consulte su base de datos o llame directamente a su API Express existente, y despliéguela en una ruta que su servidor Express no maneje. Compare el tiempo hasta obtener el primer byte y el tamaño del paquete antes de migrar páginas adicionales. Una vez validada este enfoque, traslade la autenticación a middleware.ts, y luego convierta poco a poco los flujos de trabajo con mayor tráfico en Acciones de Servidor; una migración gradual es más segura y práctica que una reescritura total. Los equipos que tienen problemas son aquellos que migran toda la base de código antes de identificar dónde los Componentes de Servidor realmente reducen el esfuerzo y dónde simplemente introducen una nueva capa conceptual sobre un sistema ya funcional.

    Lecturas relacionadas

  • Sustituyendo Jest por el ejecutor de pruebas nativo de Node en Node 24 — Una migración real muestra cómo el ejecutor de pruebas integrado en Node 24 y el soporte nativo para TypeScript reducen el tiempo de CI al mismo tiempo que se eliminan cuatro dependencias.
  • Ajuste de los nuevos controles de particionamiento de Turbopack en Next.js 16.3 — Un análisis práctico de la nueva configuración turbopackChunking en Next.js 16.3, que explica cómo maxChunkCountPerGroup y generateComponentChunks afectan el tamaño del paquete y el caché.
  • Conviirtiendo los controladores de rutas de Next.js en una capa BFF deliberada — Aprenda qué soluciona el patrón Backend for Frontend, por qué vuelve a utilizarse en aplicaciones Next.js y cómo evitar que los controladores de rutas se conviertan en objetos excesivamente complejos.