Головна / Статті

Обмін кодом між 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 у NestJS monorepo.

Від трьох репозиторіїв до одного простору роботи

Вихідною точкою зазвичай є три проекти-брати, які нічим не пов’язані між собою:

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 та спільних пакетів.