Dzielenie się kodem między NestJS, Next.js a Expo (React Native) w pnpm Turborepo
Ustal strukturę monorepo z użyciem pnpm Turborepo dla API NestJS, strony Next.js oraz aplikacji Expo React Native i zdecyduj, jakie typy i schematy powinny znaleźć się w pakietach wspólnych.
Źródła: Building a Modern Full-Stack Monorepo with Turborepo, NestJS, Next.js, and React Native
Produkt z backendem NestJS, aplikacją webową Next.js oraz aplikacją mobilną Expo (React Native) zazwyczaj zaczyna swój rozwój jako trzy repozytoria, a koszty szybko rosną. Zmieniony typ musi być kopiowany ręcznie, reguły walidacji istnieją w trzech wersjach, a klient API każdej aplikacji stopniowo odbiega od pozostałych. Monorepo Turborepo w przestrzeniach roboczych pnpm umożliwia rozgłoszenie wszystkich trzech aplikacji osobno, jednocześnie dostarczając im wspólny miejscownik dla kodu, co powinno być ujednolicone. Poniżej znajduje się schemat przestrzeni roboczej dla tej architektury oraz zasada określania, co powinno znajdować się w pakietach wspólnych; kwestia tego, czy w ogóle warto dodać Turborepo, jest omówiona w gdzie Turborepo pasuje do monorepo NestJS.
Od trzech repozytoriów do jednej przestrzeni roboczej
Punktem wyjścia są zazwyczaj trzy projekty siostrzane, które nie są ze sobą powiązane:
backend/
frontend/
mobile/
W monorepo stają się przestrzeniami roboczymi w jednym repozytorium, zarządzanymi przez Turborepo i łączonymi za pomocą pnpm:
my-project/
├── apps/
├── packages/
├── turbo.json
└── pnpm-workspace.yaml
Każda aplikacja dzieli teraz jedną instalację zależności, jeden zestaw konfiguracji TypeScript i lint oraz jedno miejsce na kod wielokrotnie używalny. W małym projekcie może to wydawać się zbędnym rytuałem. W projekcie średniej lub dużej wielkości eliminacja pracy polegającej na kopiowaniu i synchronizowaniu jest jednym z największych wzrostów produktywności, jakie można osiągnąć.
Struktura z jedną funkcją w każdej folderze
Struktura, która dobrze skaluje się w tym środowisku:
my-project/
apps/
├── api/
├── web/
└── mobile/
packages/
├── api-client/
├── config/
├── types/
├── utils/
├── validation/
└── ui/
turbo.json
package.json
pnpm-workspace.yaml
Najwyższy poziom oddziela elementy nadające się do uruchomienia od tych, które można ponownie wykorzystać, a każda folder ma jedną odpowiedzialność.
apps: programy możliwe do samodzielnego wdrożenia
apito backend NestJS.webto aplikacja Next.js.mobileto aplikacja Expo (React Native).
Każdy z nich posiada własny proces budowania, konfiguracji i wdrożenia; wspólne repozytorium nie oznacza konieczności wspólnego wysyłania tych elementów.
packages: kod, którym zgadzają się aplikacje
Tutaj właśnie pokazuje się zaleta monorepo. Logika wspólna jest pisana raz tutaj i importowana tam, gdzie jest potrzebna. Prostym przykładem jest typ domeny:
//packages/types
export interface User {
id: string;
name: string;
email: string;
}
Podaj paczce nazwę, na przykład @repo/types w pliku package.json, dodaj ją jako zależność typu workspace:* w każdym aplikacji, a wszystkie aplikacje będą importować tę samą definicję:
import { User } from "@repo/types";
Nie istnieją równoległe kopie modelu ani interfejsy, które potajemnie się różnią. Gdy User otrzymuje nowe pole, sprawdzacz typów wskazuje na wszystkie miejsca w każdej aplikacji, które muszą je obsłużyć.
Jedna schemat walidacji dla serwera i klientów
Walidacja to kolejne klasyczne źródło duplikacji. Te same zasady rejestracji często pojawiają się jako DTO w NestJS, walidator form w Next.js oraz walidator form w React Native, a prędzej czy później jeden z nich przestaje działać poprawnie.
Wspólny schemat Zod eliminuje ten problem:
import { z } from "zod";
export const RegisterSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
API weryfikuje przychodzące żądania pod kątem RegisterSchema, a oba klienty używają go do walidacji formularzy, więc wszystkie one stosują identyczne zasady. Gdy zmienia się minimalna długość hasła, zmienia się tylko jeden plik. Można również wygenerować typ TypeScript za pomocą z.infer<typeof RegisterSchema> zamiast deklarować go osobno. Należy pamiętać, że Zod 4 wprowadził pomocniki formatowania na najwyższym poziomie, takie jak z.email(); łańcuchowe wyrażenia typu z.string().email() nadal działają, ale są tam oznaczone jako przestarzałe, więc dostosuj styl do swojej wersji Zod.
Jeden pakiet klienta API
Zamiast aby każde aplikacja tworzyła własną instancję Axios, umieść jedną w dedykowanym pakiecie:
//packages/api-client
import axios from "axios";
export const api = axios.create({
baseURL: process.env.API_URL,
});
Każda aplikacja klienta importuje je z tego samego miejsca:
import { api } from "@repo/api-client";
Headyery autoryzacyjne, intercepcje, mechanizmy ponawiania prób oraz obsługa błędów są definiowane tylko raz i zachowują się tak samo w aplikacjach internetowych i mobilnych. Jedna uwaga: process.env.API_URL jest odczytywany inaczej przez każdą z frameworków. Next.js udostępnia kodowi przeglądarki tylko zmienne poprzedzone przedrostkiem NEXT_PUBLIC_, natomiast Expo włącza do pliku aplikacji wyłącznie zmienne EXPO_PUBLIC_. W praktyce prostsze jest eksportowanie funkcji, która przyjmuje jako argument adres bazowy, a każda aplikacja może przekazać swą własną skonfigurowaną wartość.
Dlaczego komponenty UI rzadko są przekazywane
Zespoły często pytają, czy komponenty React mogą być współdzielone między Next.js a React Native. Nie bezpośrednio. Aplikacja internetowa renderuje elementy DOM takie jak:
<div>
podczas gdy React Native renderuje elementy natywne, takie jak:
<View>
Oba wykorzystują React, ale ich słownictwo komponentów się różni, więc komponent napisany dla jednej platformy nie będzie renderowany na drugiej. Jeśli rzeczywistym celem jest interfejs użytkownika międzyplatformowy, biblioteki takie jak Tamagui, Solito czy React Native Web zostały stworzone właśnie w tym celu i warto je rozważyć. W przeciwnym razie należy udostępniać logikę biznesową, mechanizmy walidacji, hooki, typy oraz narzędzia wspólne dla wszystkich platform, a interfejs każdej z nich umieścić w osobnej aplikacji.
Uruchamianie środowiska pracy
Turborepo upraszcza codzienne polecenia. Dzięki korzeniowemu skryptowi dev, który wywołuje turbo dev, jedno polecenie uruchamia wszystkie aplikacje:
pnpm dev
Aby pracować nad jedną aplikacją, użyj filtru pnpm z nazwą pakietu docelowego:
pnpm --filter api dev
pnpm --filter web dev
pnpm --filter mobile start
Za tymi poleceniami Turborepo analizuje graf zależności, wykonywa zadania we właściwej kolejności i przechowuje wyniki w pamięci cache, dzięki czemu pakiety współdzielone są budowane przed aplikacjami, które od nich zależą, a praca, która nie wymaga zmian, jest pomijana.
Zasada ogólna dotycząca tego, co powinno znajdować się w pakietach
Dziel się logiką; nie wymuszaj współdzielonej interfejsu użytkownika. Te elementy dobrze funkcjonują pomiędzy aplikacjami:
- Typy i enumy TypeScript
- Schematy Zod
- Funkcje pomocnicze i stałe
- Klient API i narzędzia autoryzacji
- Logika biznesowa
- Wspólna konfiguracja (TypeScript, ESLint, formatowanie)
- Biblioteki wewnętrzne
Komponenty interfejsu użytkownika powinny znajdować się w wspólnym pakiecie tylko wtedy, gdy projekt został celowo stworzony na bazie rozwiązania interfejsu wieloplatformowego. Również kod przeznaczony wyłącznie dla serwera, taki jak dostęp do bazy danych czy obsługa tajemnic, powinien być wykluczony z pakietów importowanych przez aplikacje klienckie, w przeciwnym razie może trafić do pliku przeglądarki lub aplikacji mobilnej.
Dlaczego granice są kluczowe
Wartość tego układu wynika z jego granic. Aplikacje pozostają na tyle niezależne, by rozwijać się i wdrażać według własnego harmonogramu, podczas gdy umowa między nimi, typy, schematy oraz klient API znajdują się dokładnie w jednym miejscu. Zmiany rozprzestrzeniają się poprzez sprawdzacz typów zamiast poprzez kopiowanie i wklejanie, a przestrzeń robocza pozostaje łatwa do zarządzania w miarę rozwoju bazy kodu.
Podobne materiały
- Gdzie mieści się Turborepo w monorepo NestJS i kiedy nie należy go używać: Jak Turborepo, przestrzenie robocze pnpm oraz NestJS dzielą zadania w monorepo typu TypeScript: wspólne pakiety, grafy zadań, cacheowanie, ryzyko powiązań oraz sytuacje, gdy należy go pominąć.
- Automatyczne wygenerowanie bezpiecznego klienta API Next.js z NestJS Swagger: Dowiedz się, jak usunąć duplikowane typy API przy użyciu NestJS Swagger i Orval do automatycznego tworzenia bezpiecznych hooków React Query dla Next.js.