Compartir código entre NestJS, Next.js y Expo (React Native) en un pnpm Turborepo
Diseñe un monorepo con pnpm Turborepo para una API de NestJS, un sitio de Next.js y una aplicación de Expo React Native, y decida qué tipos y esquemas irán en los paquetes compartidos.
Fuentes: Building a Modern Full-Stack Monorepo with Turborepo, NestJS, Next.js, and React Native
Un producto que cuenta con un backend de NestJS, una aplicación web de Next.js y una aplicación móvil de Expo (React Native) suele comenzar como tres repositorios, y el costo se hace evidente rápidamente. Un tipo modificado debe copiarse a mano, las reglas de validación existen en tres versiones, y el cliente API de cada aplicación se aleja silenciosamente de los demás. Un monorepo de Turborepo en espacios de trabajo de pnpm permite que las tres aplicaciones se desplieguen por separado, al mismo tiempo que brinda un único lugar para el código del cual deberían estar de acuerdo. A continuación se muestra la estructura de un espacio de trabajo para esta tecnología y una regla para decidir qué contenido debe incluirse en los paquetes compartidos; si realmente vale la pena añadir Turborepo se explica en dónde encaja Turborepo en un monorepo de NestJS.
De tres repositorios a un solo espacio de trabajo
El punto de partida suele ser tres proyectos hermanos que no están conectados entre sí:
backend/
frontend/
mobile/
En un monorepo se convierten en espacios de trabajo dentro de un único repositorio, orquestados por Turborepo y vinculados mediante pnpm:
my-project/
├── apps/
├── packages/
├── turbo.json
└── pnpm-workspace.yaml
Todas las aplicaciones ahora comparten una única instalación de dependencias, un mismo conjunto de configuraciones de TypeScript y lint, y un único lugar para el código reutilizable. En un proyecto pequeño, esto puede parecer un trámite innecesario. En uno de tamaño medio o grande, eliminar el trabajo de copiar y sincronizar es una de las mayores mejoras en productividad que existen.
Un diseño con una tarea por carpeta
Una estructura que se adapta bien a esta tecnología:
my-project/
apps/
├── api/
├── web/
└── mobile/
packages/
├── api-client/
├── config/
├── types/
├── utils/
├── validation/
└── ui/
turbo.json
package.json
pnpm-workspace.yaml
El nivel superior separa los elementos ejecutables de los reutilizables, y cada carpeta tiene una única responsabilidad.
apps: programas que se pueden desplegar de forma independiente
apicorresponde al backend de NestJS.webes la aplicación de Next.js.mobilees la app de Expo (React Native).
Cada uno mantiene su propio proceso de compilación, configuración y despliegue; compartir un repositorio no implica que se envíen juntos.
packages: código acordado por las aplicaciones
Aquí es donde el monorepo resulta útil. La lógica compartida se escribe una sola vez aquí e se importa donde sea necesario. Un caso sencillo es un tipo de dominio:
//packages/types
export interface User {
id: string;
name: string;
email: string;
}
Asigne un nombre al paquete, como @repo/types, en su package.json; ágüelo como dependencia workspace:* en cada aplicación, y así todas las aplicaciones importarán la misma definición:
import { User } from "@repo/types";
No existen copias paralelas del modelo ni interfaces que diverjan silenciosamente. Cuando User adquiere un campo, el verificador de tipos señala cada lugar en cada aplicación que necesita manejarlo.
Un único esquema de validación para servidores y clientes
La validación es otra fuente clásica de duplicación. Las mismas reglas de registro suelen aparecer como un DTO de NestJS, un validador de formularios de Next.js y un validador de formularios de React Native, y tarde o temprano uno de ellos deja de funcionar correctamente.
Un esquema Zod compartido elimina el problema:
import { z } from "zod";
export const RegisterSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
La API valida las solicitudes recibidas en relación con RegisterSchema, y ambos clientes la utilizan para la validación de formularios, por lo que aplican reglas idénticas. Si cambia la longitud mínima de la contraseña, solo es necesario modificar un archivo. También se puede obtener el tipo de TypeScript con z.infer<typeof RegisterSchema> en lugar de declararlo por separado. Cabe señalar que Zod 4 introdujo ayudantes de formato de nivel superior como z.email(); la forma encadenada z.string().email() sigue funcionando, pero está marcada como obsoleta, así que adapte su estilo a la versión de Zod que utilice.
Un único paquete de cliente API
En lugar de que cada aplicación cree su propia instancia de Axios, colóquela en un paquete dedicado:
//packages/api-client
import axios from "axios";
export const api = axios.create({
baseURL: process.env.API_URL,
});
Toda aplicación de cliente la importa desde el mismo lugar:
import { api } from "@repo/api-client";
Los encabezados de autenticación, los interceptores, así como el manejo de intentos repetidos y errores se definen una sola vez y funcionan de la misma manera en la web y en dispositivos móviles. Una advertencia: process.env.API_URL se lee de manera diferente por cada framework. Next.js solo expone las variables precedidas por NEXT_PUBLIC_ al código del navegador, mientras que Expo incluye solo las variables EXPO_PUBLIC_ directamente en el paquete de la aplicación. En la práctica, es más limpio exportar una función que tome la URL base como argumento y permitir que cada aplicación proporcione su propio valor configurado.
Por qué los componentes de UI rara vez se transfieren
Con frecuencia, los equipos preguntan si los componentes de React pueden compartirse entre Next.js y React Native. No directamente. La aplicación web renderiza elementos DOM como los siguientes:
<div>
Mientras que React Native renderiza primitivas nativas como:
<View>
Ambos utilizan React, pero los vocabularios de componentes difieren, por lo que un componente escrito para una plataforma no se renderiza en la otra. Si el objetivo real es una interfaz de usuario multiplataforma, existen bibliotecas como Tamagui, Solito o React Native Web diseñadas para ello y que merecen ser evaluadas. De lo contrario, comparta la lógica de negocio, la validación, los hooks, los tipos y las utilidades, y mantenga la interfaz de usuario de cada plataforma en su propia aplicación.
Ejecutar el espacio de trabajo
Turborepo simplifica los comandos cotidianos. Con un script raíz dev que llama a turbo dev, un único comando inicia todas las aplicaciones:
pnpm dev
Para trabajar en una aplicación, utilice el filtro de pnpm con el nombre del paquete objetivo:
pnpm --filter api dev
pnpm --filter web dev
pnpm --filter mobile start
Detrás de esas instrucciones, Turborepo lee el grafo de dependencias, ejecuta las tareas en el orden correcto y almacena en caché los resultados, de modo que los paquetes compartidos se compilan antes que las aplicaciones que dependen de ellos y se omiten las tareas que no han cambiado.
Una regla práctica sobre qué debe incluirse en los paquetes
Comparta la lógica; no fuerce la interfaz de usuario compartida. Estos elementos se integran bien entre aplicaciones:
- Tipos y enums de TypeScript
- Estructuras Zod
- Funciones y constantes utilitarias
- El cliente de API y los ayudantes de autenticación
- La lógica de negocio
- Configuración compartida (TypeScript, ESLint, formato)
- Bibliotecas internas
Los componentes de interfaz de usuario solo deben encontrarse en un paquete compartido cuando el proyecto se haya creado intencionalmente sobre una solución de interfaz multiplataforma. Además, mantenga el código exclusivo para el servidor, como el acceso a bases de datos o el manejo de secretos, fuera de los paquetes que importan las aplicaciones cliente, ya que de lo contrario podría terminar en un paquete para navegador o móvil.
Por qué los límites son importantes
El valor de este diseño proviene de sus límites. Las aplicaciones permanecen lo suficientemente independientes como para evolucionar e implementarse según sus propios plazos, mientras que el contrato entre ellas, los tipos, los esquemas y el cliente de API se encuentran en un único lugar. Los cambios se propagan a través del verificador de tipos en lugar de mediante copiar y pegar, y el espacio de trabajo se mantiene manejable a medida que crece la base de código.
Leer más
- Dónde encaja Turborepo en un monorepo de NestJS y cuándo dejarlo de lado: Cómo Turborepo, los espacios de trabajo de pnpm y NestJS dividen el trabajo en un monorepo de TypeScript: paquetes compartidos, gráficos de tareas, caché, riesgos de acoplamiento y cuándo omitirlo.
- Generar automáticamente un cliente API de Next.js seguro de tipos a partir de NestJS Swagger: Aprenda cómo eliminar tipos de API duplicados utilizando NestJS Swagger y Orval para generar automáticamente ganchos React Query seguros de tipos para Next.js.