Главная / Статьи

Обмен кодом между NestJS, Next.js и Expo (React Native) в pnpm Turborepo

Создайте monorepo с использованием pnpm Turborepo для API на NestJS, веб-сайта на Next.js и приложения на Expo React Native, а также определите, какие типы и схемы будут находиться в общих пакетах.

Издатель: REACTAPP.TOP5 мин чтения1135 слов

Источники: Building a Modern Full-Stack Monorepo with Turborepo, NestJS, Next.js, and React Native

Продукт с бэкендом NestJS, веб-приложением Next.js и мобильным приложением Expo (React Native) обычно начинает свою разработку с трех репозиториев, и затраты быстро растут. Изменённый тип данных копируется вручную, правила валидации существуют в трёх версиях, а API-клиенты каждого приложения постепенно отдаляются друг от друга. Monorepo Turborepo в рамках рабочих пространств pnpm позволяет развергать все три приложения отдельно, одновременно обеспечивая единое хранилище кода, который должен быть общим для них. Ниже приведена структура рабочего пространства для такой стек-технологий и правило для определения того, что должно находиться в общих пакетах; вопрос о том, стоит ли вообще использовать Turborepo, рассматривается в статье где место Turborepo в monorepo NestJS.

От трёх репозиториев к одному рабочему пространству

Исходной точкой обычно являются три проекта-брата, которые не связаны между собой ничем:

backend/
frontend/
mobile/

В монорепозитории они превращаются в рабочие пространства в одном репозитории, управляемые Turborepo и связанные с помощью pnpm:

my-project/
├── apps/
├── packages/
├── turbo.json
└── pnpm-workspace.yaml

Теперь каждое приложение использует одну установку зависимостей, один набор настроек TypeScript и lint, а также одно место для повторно используемого кода. В небольших проектах это может казаться излишней формальностью. В средних или крупных проектах устранение необходимости копирования и синхронизации — один из самых значительных приростов производительности.

Структура с одной задачей на папку

Структура, хорошо подходящая для этой технологической стековой сборки:

my-project/
apps/
├── api/
├── web/
└── mobile/
packages/
├── api-client/
├── config/
├── types/
├── utils/
├── validation/
└── ui/
turbo.json
package.json
pnpm-workspace.yaml

Верхний уровень разделяет элементы, предназначенные для выполнения, и элементы, предназначенные для повторного использования, причем каждая папка имеет свою единственную функцию.

apps: программы, которые можно развернуть независимо

  • api — это бэкенд NestJS.
  • web — это приложение Next.js.
  • mobile — это приложение Expo (React Native).

Каждый из этих компонентов имеет собственную процедуру сборки, конфигурации и развертывания; наличие общего репозитория не означает их совместную доставку.

packages: код, согласованный всеми приложениями

Именно здесь проявляется преимущество монорепозитория. Общая логика пишется здесь один раз и импортируется там, где она нужна. Простым примером может служить тип домена:

//packages/types

export interface User {
  id: string;
  name: string;
  email: string;
}

Укажите имя пакета, например @repo/types, в его файле package.json, добавьте его в качестве зависимости с параметром workspace:* в каждом приложении, и каждое приложение будет импортировать одно и то же определение:

import { User } from "@repo/types";

Не существует параллельных копий модели, а также интерфейсов, которые бы тайно отклонялись друг от друга. Когда у объекта User появляется новое поле, проверщик типов указывает на все места во всех приложениях, где необходимо им управлять.

Одна схема валидации для сервера и клиентов

Валидация — ещё одна классическая причина дублирования. Одни и те же правила регистрации часто реализуются в виде DTO для NestJS, валидатора форм для Next.js и валидатора форм для React Native, и рано или поздно кто-то из них начинает работать некорректно.

Общая схема Zod устраняет эту проблему:

import { z } from "zod";

export const RegisterSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

API проверяет поступающие запросы согласно RegisterSchema, и оба клиента используют его для проверки форм, поэтому все они соблюдают одинаковые правила. Если меняется минимальная длина пароля, меняется только один файл. Также можно получить тип TypeScript с помощью z.infer<typeof RegisterSchema> вместо отдельной его декларации. Обратите внимание, что в Zod 4 были введены вспомогательные функции для форматирования на верхнем уровне, такие как z.email(); цепочка функций z.string().email() по-прежнему работает, но там считается устаревшей, поэтому выбирайте стиль в соответствии с вашей версией Zod.

Один пакет клиента API

Вместо того чтобы каждое приложение создавало собственный экземпляр Axios, поместите его в отдельный пакет:

//packages/api-client

import axios from "axios";

export const api = axios.create({
  baseURL: process.env.API_URL,
});

Каждое приложение-клиент импортирует его из одного и того же места:

import { api } from "@repo/api-client";

Заголовки аутентификации, интерцепторы, механизмы повторных попыток и обработки ошибок определяются один раз и работают одинаково как в веб-приложениях, так и в мобильных приложениях. Однако существует одно ограничение: переменная process.env.API_URL читается по-разному в каждой фреймворковой среде. Next.js предоставляет в код браузера только переменные с префиксом NEXT_PUBLIC_, тогда как Expo встраивает в пакет приложения только переменные с префиксом EXPO_PUBLIC_. На практике проще экспортировать функцию-фабрику, принимающую базовый URL в качестве аргумента, чтобы каждое приложение могло передавать своё собственное настроенное значение.

Почему компоненты UI редко передаются

Команды часто спрашивают, можно ли делить компоненты React между Next.js и React Native. Непосредственно — нет. Веб-приложение отрисовывает элементы DOM, такие как:

<div>

в то время как React Native отрисовывает нативные примитивы, такие как:

<View>

Обе платформы используют React, но их словари компонентов отличаются, поэтому компонент, написанный для одной платформы, не будет отображаться на другой. Если цель — кроссплатформенный интерфейс, то для этой цели созданы библиотеки вроде Tamagui, Solito или React Native Web, которые стоит рассмотреть. В противном случае следует делиться бизнес-логикой, механизмами валидации, хуками, типами и утилитами, а интерфейс каждой платформы оставлять в отдельном приложении.

Запуск рабочей среды

Turborepo упрощает повседневные команды. Благодаря корневому скрипту dev, который вызывает turbo dev, одна команда запускает все приложения:

pnpm dev

Чтобы работать над одним приложением, используйте фильтр pnpm с именем целевого пакета:

pnpm --filter api dev
pnpm --filter web dev
pnpm --filter mobile start

За этими командами Turborepo анализирует граф зависимостей, выполняет задачи в правильном порядке и кэширует результаты, благодаря чему общие пакеты собираются раньше приложений, которые от них зависят, а повторная обработка неизменных данных исключается.

Ориентир, что следует включать в пакеты

Делитесь логикой; не насаждайте общий интерфейс. Это хорошо переносится между приложениями:

  • Типы и перечисления TypeScript
  • Шаблоны Zod
  • Вспомогательные функции и константы
  • Клиент API и инструменты аутентификации
  • Бизнес-логика
  • Общая конфигурация (TypeScript, ESLint, форматирование)
  • Внутренние библиотеки

Компоненты интерфейса пользователя должны находиться в общем пакете только тогда, когда проект специально создавался с использованием кроссплатформенного решения для интерфейса. Также следует не включать код, предназначенный исключительно для сервера, такой как доступ к базе данных или обработка конфиденциальных данных, в пакеты, которые импортируются клиентскими приложениями, иначе он может оказаться в браузерном или мобильном пакете.

Почему важны границы

Ценность такой структуры заключается в её границах. Приложения остаются достаточно независимыми, чтобы развиваться и развертываться по собственным графикам, в то время как контракт между ними, типы, схемы и клиент API находятся в одном месте. Изменения распространяются через проверку типов, а не путем копирования и вставки, что позволяет сохранять управляемость рабочим пространством по мере роста кодовой базы.

Связанные материалы

  • Пошаговое создание монорепозитория с pnpm и Turborepo для приложений Node.js: настройка монорепозитория на TypeScript из пустой папки с использованием рабочих пространств pnpm и Turborepo, а затем выполнение, сборка и фильтрация задач для веб-приложения, API и общих пакетов.