Dlaczego monorepo i po co łączyć Nx, Turborepo oraz pnpm
Monorepo vs multi‑repo – gdzie naprawdę boli
W typowym układzie multi‑repo każdy projekt żyje w swoim własnym repozytorium Git. Osobny backend, osobny panel admina, osobny landing, osobny design system. Na początku wygląda to porządnie, bo „każdy ma swoje piaskownice”. Problemy wychodzą, gdy projektów przybywa i trzeba utrzymać między nimi spójność.
Najczęstsze bóle multi‑repo to:
- rozjeżdżające się wersje – biblioteka UI w wersji 2.3.0 w jednym repo, 2.1.1 w innym, ktoś poprawił bug tylko w najnowszej, a stara dalej żyje w produkcji,
- zmiany przekrojowe – jedna modyfikacja kontraktu API backendu wymaga PR‑ów w 3–4 repo, koordynacji releasów i ręcznego pilnowania kolejności wdrożeń,
- duplikacja konfiguracji – osobne pliki tsconfig, ESLint, Prettier, pipeline’y CI, konwencje – wszystko trzeba powielać i utrzymywać,
- utrudniony onboarding – nowa osoba w zespole musi poznać strukturę kilku repozytoriów, różne skrypty, różne procesy developmentu,
- trudne dzielenie się kodem – małe, wewnętrzne helpery szybciej się kopiuje między repo niż pakuje w osobny pakiet npm i publikuje.
Monorepo odwraca tę perspektywę: cały kod biznesowy jednego produktu lub organizacji ląduje w jednym repozytorium. Frontend, backend, biblioteki wspólne, narzędzia – wszystko współdzieli historię Git, jedną konfigurację CI i jedną przestrzeń zależności. Ruchy między pakietami robi się lokalnie, bez wypychania czegokolwiek do zewnętrznego rejestru.
Jakie problemy realnie rozwiązuje monorepo w zespole
Monorepo daje największą wartość tam, gdzie istnieje dużo współdzielonego kodu i częste zmiany przekrojowe. Kilka kluczowych korzyści:
- wspólny kod „pod ręką” – design system, utilsy, typy TypeScript, kontrakty API, schematy danych – wszystko w jednym drzewie katalogów, bez publikowania na zewnętrzny registry,
- jedna wersja prawdy – jedna wersja biblioteki UI, jeden config ESLint czy Prettier używany przez wszystkie aplikacje,
- łatwiejsze zmiany przekrojowe – jedna gałąź Git z refaktoryzacją, jednen PR obejmujący backend, frontend i biblioteki, widoczna całość wpływu zmiany,
- lepsza widoczność zależności – w narzędziach takich jak Nx dependency graph pokazuje, które aplikacje korzystają z której biblioteki,
- spójne procesy – te same skrypty
build,test,lintwe wszystkich pakietach, jednolity pipeline CI/CD.
Monorepo nie jest panaceum – można w nim wygenerować „spaghetti w jednym repo”. Klucz tkwi w tym, jak zostanie zaprojektowana architektura pakietów i jakich narzędzi użyje się do zarządzania zależnościami i zadaniami.
Rola Nx, Turborepo i pnpm workspaces – wysoki poziom
Połączenie Nx, Turborepo i pnpm workspaces może brzmieć jak przesada, ale te trzy narzędzia działają na różnych warstwach:
- pnpm workspaces – menedżer zależności i workspace’ów; odpowiada za instalowanie paczek, linkowanie lokalnych pakietów, izolację
node_modules, - Nx – task runner, narzędzie architektoniczne i DX; ma dependency graph, komendy
affected, schematy generatorów, executors dla budowania, testów i lintowania, - Turborepo – silnik pipeline’u zadań z cache’owaniem; skupia się na definiowaniu kroków typu
build,testi agresywnym cache’owaniu ich wyników lokalnie i zdalnie.
Łącząc je, pnpm staje się fundamentem zarządzania dependencies, Nx wprowadza strukturę projektów i inteligentne uruchamianie tylko tego, co wymaga przebudowy, a Turborepo może pełnić rolę prostego, bardzo wydajnego silnika pipeline’u – zwłaszcza dla buildów produkcyjnych i CI.
Dlaczego łączyć, a nie wybierać jedno narzędzie
Teoretycznie można zbudować monorepo tylko na:
- samym pnpm (plus ręczne skrypty),
- pnpm + Turborepo,
- pnpm + Nx.
Praktyka pokazuje jednak, że:
- pnpm rozwiązuje dependency management, ale nie wie nic o tym, że pakiet A zależy od pakietu B w sensie architektury projektu,
- Nx ma świetny dependency graph i komendy
affected, ale jego konfiguracja bywa cięższa dla małych zespołów i repo, - Turborepo ma bardzo prostą konfigurację pipeline’u, ale jest mniej opiniotwórcze w kwestii architektury i dependency graphu (w porównaniu z Nx).
Połączenie Nx i Turborepo w jednym monorepo z pnpm jako fundamentem pozwala:
- wykorzystać pnpm workspaces do wydajnego zarządzania zależnościami,
- pozyskać z Nx dependency graph, reguły architektoniczne, komendy
affected:<target>, - i używać Turborepo jako prostego, zrozumiałego dla całego zespołu silnika pipeline’ów z bardzo dobrym cache’owaniem, np. w CI.
Taki zestaw jest sensowny w większych organizacjach i projektach, gdzie zespół chce mieć i mocne narzędzie architektoniczne (Nx), i lekki pipeline z cache (Turbo), a jednocześnie nie rezygnować z korzyści pnpm workspaces.
Przykładowy scenariusz firmy z wieloma aplikacjami
Wyobraź sobie zespół, który rozwija:
- aplikację panel admina (React + Next.js),
- publiczny landing marketingowy (Next.js),
- wewnętrzne API Node/NestJS,
- wspólny design system z komponentami React i tokenami,
- kilka bibliotek domenowych z logiką biznesową (np. obsługa płatności, raportów).
Bez monorepo każdy z tych elementów to osobne repo, osobny proces wdrożeniowy, różne wersje bibliotek i konfiguracji. Z monorepo i zestawem Nx + Turborepo + pnpm:
- design system i biblioteki domenowe stają się lokalnymi pakietami wykorzystywanymi przez admina, landing i API,
- zmiana komponentu w design systemie może automatycznie uruchomić build i testy tylko w appkach, które z niego korzystają (Nx
affected), - cache z Turborepo sprawia, że kolejny CI przy podobnej zmianie jest dużo szybszy,
- pnpm zapewnia spójne, zlinkowane dependencies bez „node_modules-horroru”.
Podstawy: jak myśleć o strukturze monorepo
Prosty słownik: pakiet, apka, biblioteka, narzędzia
Żeby się nie pogubić, dobrze jest używać kilku prostych pojęć:
- pakiet (package) – najmniejsza jednostka udostępniana w monorepo; ma swój
package.json, własne skrypty i zależności, - apka (app) – pakiet, który jest samodzielną aplikacją uruchamianą przez użytkownika; np. frontend, backend, worker, CLI,
- biblioteka (lib) – pakiet z kodem współdzielonym: komponenty UI, logika domenowa, utilsy, definicje typów,
- narzędzia (tools) – pakiety, które wspierają development, ale nie są bezpośrednio produkcyjne; np. generatory kodu, skrypty migracyjne, własne CLI.
Technicznie wszystko jest „pakietem”, ale to rozróżnienie pomaga przy ustalaniu zależności i projektowaniu architektury. Aplikacje zazwyczaj zależą od bibliotek i narzędzi, biblioteki mogą zależeć od innych bibliotek, a narzędzia często stoją z boku.
Wzorce organizacji katalogów w monorepo
Najpopularniejsze wzorce struktury monorepo w ekosystemie JavaScript/TypeScript to:
apps/+packages/– rozdzielenie aplikacji i bibliotek:apps/admin,apps/landing,apps/api,packages/ui,packages/utils,packages/payments.
packages/tylko – wszystkie pakiety w jednym katalogu; aplikacje to po prostu pakiety:packages/admin-app,packages/landing-app,packages/api-service,packages/ui,packages/domain-payments.
W kontekście Nx często spotyka się strukturę apps/ i libs/. W połączeniu z pnpm workspaces można to bez problemu odwzorować, np. definiując workspace’y apps/* i libs/*. Najważniejsze, aby zespół rozumiał różnice między tymi segmentami.
Monorepo oparte na workspaces – rola workspace managera
Workspace manager (w naszym przypadku pnpm) zajmuje się wyłącznie:
- definicją, które katalogi są pakietami,
- instalacją zależności dla wszystkich pakietów na raz,
- linkowaniem lokalnych pakietów między sobą.
Workspace manager nie decyduje o tym:
- jak budowane są aplikacje (jakie komendy
builduruchomić), - jak wygląda dependency graph na poziomie architektury,
- które projekty są powiązane biznesowo,
- jak działa cache zadań.
Do tego potrzebne są kolejne warstwy – właśnie Nx i Turborepo. Dobrze jest mieć to w głowie: pnpm to fundament, ale cała „inteligencja” życia monorepo dzieje się wyżej.
Granice między core a feature packages
Rozsądne pocięcie kodu na pakiety jest kluczowe. Zbyt mało pakietów – duże, ociężałe moduły, trudno dzielić się kodem. Zbyt dużo – ciężko ogarnąć zależności, każdy feature na osobną paczkę generuje chaos.
Pomaga proste rozróżnienie:
- pakiety „core” – fundamenty, które rzadko się zmieniają:
@org/eslint-config,@org/tsconfig,@org/ui(design system),@org/domain-core– wspólne typy i abstrakcje domenowe.
- pakiety „feature” – wyspecjalizowane moduły dla konkretnych obszarów:
@org/feature-payments,@org/feature-reports,@org/feature-auth.
Core powinien mieć minimalne zależności w górę (raczej niczego nie importuje z feature’ów), natomiast feature’y mogą korzystać z core i z siebie nawzajem według jasno zdefiniowanych zasad.
Projektowanie zależności bez „spaghetti w jednym repo”
Architektura monorepo jest stabilna wtedy, gdy da się ją narysować jako graf skierowany bez cykli (DAG – Directed Acyclic Graph). Kilka prostych reguł:
- aplikacje na górze, biblioteki i narzędzia niżej,
- zakaz importów „w dół” – core nie importuje feature, design system nie importuje konkretnych ekranów aplikacji,
- logika domenowa powinna być wyżej niż warstwy techniczne UI (kontrolery, komponenty),
- każda warstwa powinna znać tylko warstwy „niższe”, nie wyższe.
Nx potrafi wymuszać te zasady za pomocą reguł ESLint (tzw. enforce module boundaries), ale nawet bez tego warto trzymać prosty model w głowie: strzałki zależności zawsze w dół.
Przegląd narzędzi: Nx, Turborepo, pnpm – mocne i słabe strony
pnpm workspaces – szybkie, oszczędne, przewidywalne
pnpm to menedżer pakietów, który rozwiązuje kilka realnych problemów znanych z npm i yarn:
- izolacja
node_modules– zamiast kopiować każdą paczkę osobno, pnpm używa symlinków z globalnego store; to oszczędza miejsce i przyspiesza instalację,
pnpm w praktyce monorepo
W codziennym użyciu pnpm workspaces sprowadzają się do kilku prostych nawyków. Zamiast wchodzić do każdego pakietu i odpalać npm install, wykonujesz jedną komendę w katalogu głównym. Zamiast ręcznie linkować lokalne paczki, pnpm automatycznie spina zależności na podstawie nazw i wersji w package.json.
Przy dobrze skonfigurowanych workspace’ach:
- instalacja nowych zależności do danego pakietu jest przewidywalna (
pnpm add lodash --filter @org/feature-payments), - współdzielone dependency (np.
react,typescript) lądują raz w store, a nie w każdym pakiecie z osobna, - lokalne pakiety publikowane później do npm mogą już w monorepo mieć takie same nazwy i wersje.
Przy dużym zespole ważne jest, aby ustalić kilka zasad pracy z pnpm: kto może zmieniać wersje globalnych zależności, kiedy aktualizujemy wszystko hurtem, a kiedy tylko pojedynczy pakiet. To proste ustalenia, ale potrafią zaoszczędzić wiele nerwów.
Nx – narzędzie architekta monorepo
Nx wnosi do monorepo przede wszystkim „świadomość” projektu. Nie tylko odpala skrypty, ale też wie, który projekt zależy od którego. Dzięki temu potrafi odpowiedzieć na pytanie: „co muszę przebudować po tej zmianie?”.
Najważniejsze cechy Nx w takim zestawie to:
- dependency graph – wykres zależności między aplikacjami i bibliotekami, generowany automatycznie na podstawie importów i konfiguracji,
- targets – znormalizowane zadania (np.
build,test,lint) przypisane do projektów, - komendy
nx affected:<target>– uruchamianie zadań tylko tam, gdzie zmienił się kod lub zależności, - reguły granic modułów – integracja z ESLint, która blokuje niedozwolone importy między pakietami.
W praktyce sprowadza się to do takiego scenariusza: ktoś dotyka kodu w @org/feature-payments, developer przed pushem uruchamia nx affected:test, a Nx buduje graf i odpalą testy tylko w tych projektach, które faktycznie mogą ucierpieć. Szybciej, mniej hałasu, a jednocześnie mniejsze ryzyko, że coś ominiesz.
Turborepo – prosty silnik pipeline’ów
Turborepo można traktować jak zaawansowaną wersję npm run dla całego monorepo. Deklarujesz w jednym pliku, jakie zadania istnieją (np. build, lint, test), jak są od siebie zależne oraz co może się cache’ować. Turbo nie interesuje biznesowa architektura repo; patrzy na zadania i ich wejścia/wyjścia.
Dlatego świetnie się sprawdza jako „przekaźnik” między ludźmi a CI/CD:
- zespół ma jeden, krótki plik
turbo.json, - w CI wywołujesz zwykle jedną komendę, np.
pnpm turbo run lint test build, - Turbo buforuje (cache’uje) wyniki zadań lokalnie i zdalnie, co skraca kolejne pipeline’y.
W połączeniu z Nx typowy pattern wygląda tak: Nx wylicza, które projekty i targets są „dotknięte” zmianą, a Turbo dostaje już tylko listę zadań do zoptymalizowania i zcache’owania.
Jak te narzędzia się uzupełniają
W uproszczeniu:
- pnpm – zarządza paczkami i zależnościami,
- Nx – zarządza projektami, grafem i „co się powinno uruchomić”,
- Turborepo – zarządza wykonywaniem zadań i cache’em.
Dobrze ustawione trio przypomina nieco układ: system plików (pnpm), kompilator z analizą zależności (Nx) oraz system buildów z cache (Turbo). Każde robi swoje, ale na wspólnym gruncie – monorepo z paczkami pnpm.

Projekt startowy: jak zaplanować monorepo zanim ruszy kod
Mapa domeny zamiast drzewa katalogów
Najczęstszy błąd przy starcie monorepo to zaczynanie od struktury folderów. Zdecydowanie lepiej zacząć od klasycznego pytania architektonicznego: jakie mamy domeny biznesowe i aplikacje?
Dobrym punktem wyjścia jest prosta mapa:
- wypisanie wszystkich aplikacji (frontendy, backendy, worker’y, CLI),
- wypisanie wspólnych zasobów: UI, logika domenowa, konfiguracja, narzędzia,
- naszkicowanie, które aplikacje wchodzą w interakcję z którymi domenami.
Na tej podstawie da się już zaplanować pierwszy podział na core i feature packages oraz zastanowić, jakie reguły zależności będą sensowne. Nx później tylko te założenia sformalizuje.
Minimalny zestaw pakietów na start
Kuszące jest pokrojenie wszystkiego bardzo drobno od razu, ale przy pierwszej wersji monorepo lepiej utrzymać rozsądne minimum. Przykładowy zestaw startowy:
apps/admin– panel administracyjny,apps/api– backend REST/GraphQL,packages/ui– podstawowe komponenty UI i theme,packages/domain-core– typy, modele, kontrakty,packages/tools-scripts– wspólne skrypty (np. migracje, generatory).
Dopiero gdy kod zacznie rosnąć, pojawia się potrzeba wydzielenia kolejnych feature’owych pakietów: @org/feature-billing, @org/feature-auth i tak dalej. Dzięki temu struktura rośnie organicznie, a nie z góry wymyśloną siatką, która nie pasuje do rzeczywistości.
Standardy kodu i konfiguracji od pierwszego dnia
Monorepo szybko ujawnia brak spójności. Jeżeli jedna apka ma inną wersję TypeScript niż reszta, a ESLint w każdym katalogu konfiguruje się osobno, chaos pojawi się po kilku sprintach. Wspólny zestaw core config rozwiązuje większość takich problemów.
Na poziomie projektu startowego dobrze jest od razu mieć:
- wspólny
@org/eslint-configi@org/tsconfig, - ustaloną wersję
react,typescript, test runnera, - jednolite reguły formatowania (Prettier lub odpowiednik).
Te pakiety i konfiguracje staną się bazą dla wszystkich nowych apps i libs. Dzięki pnpm utrzymuje się je w jednym miejscu, a Nx popycha ich aktualizacje tylko tam, gdzie potrzeba.
Plan na CI/CD zanim monorepo urośnie
Drugim newralgicznym punktem są pipeline’y. Nawet proste monorepo warto zaprojektować pod kątem CI z myślą o skali. Kilka decyzji dobrze podjąć od razu:
- czy pipeline’y są per aplikacja, czy wspólne dla całego repo,
- jakie minimalne kroki musi przejść każda zmiana (lint, test, build),
- gdzie przechowywany będzie cache Turborepo (np. S3, Remote Cache od Vercel).
Typowy schemat: jedno repo, jeden pipeline główny, w którym używany jest Nx do określenia zakresu, a Turbo do równoległego wykonania zadań i cache. Ustawienie tego na początku oszczędza późniejszego refaktoryzowania wszystkich jobów w CI.
Konfiguracja pnpm workspaces jako fundamentu
Podstawowa konfiguracja pliku pnpm-workspace.yaml
W centrum monorepo z pnpm stoi plik pnpm-workspace.yaml. To on mówi pnpm, które katalogi należy uznać za pakiety. Przykładowa konfiguracja dla struktury apps/ + packages/:
packages:
- "apps/*"
- "packages/*"
Każdy katalog pasujący do globów w sekcji packages jest traktowany jako osobny pakiet, pod warunkiem że zawiera package.json. Dzięki temu pnpm install wykonane w katalogu głównym obsłuży wszystko naraz.
Konwencje nazewnicze pakietów
Stabilne monorepo korzysta z przewidywalnych nazw pakietów. Scoped packages (z przedrostkiem organizacji) szczególnie ułatwiają życie:
- aplikacje:
@org/admin,@org/api, - biblioteki core:
@org/ui,@org/domain-core, - feature’y:
@org/feature-payments,@org/feature-auth.
Takie nazwy trafiają bezpośrednio do name w package.json każdego pakietu. Gdy aplikacja @org/admin dodaje zależność do design systemu, po prostu wpisuje "@org/ui": "workspace:*"; pnpm zinterpretuje to jako lokalne powiązanie z innym pakietem w monorepo.
workspace:* i inne zakresy wersji
Specjalne wersje w stylu "workspace:*", "workspace:^" lub "workspace~" pozwalają pnpm rozróżnić zależności lokalne od zewnętrznych. Kilka praktycznych przykładów:
"@org/ui": "workspace:*"– dowolna wersja lokalnego@org/ui,"@org/domain-core": "workspace:^"– wersja kompatybilna semantycznie z główną wersją,"lodash": "^4.17.0"– klasyczna zależność z rejestru npm.
Dzięki temu łatwiej aktualizować wszystkie lokalne pakiety jednocześnie, utrzymując spójność wersji i unikając niespodzianek przy publikacji.
Zarządzanie zależnościami wspólnymi i lokalnymi
Naturalne jest, że część zależności pojawia się w wielu pakietach (np. React, test runner), a część jest specyficzna dla danego modułu (np. SDK do zewnętrznej usługi tylko w jednym feature). Sprawdza się takie podejście:
- najczęściej używane zależności trzymać w
package.jsonw katalogu głównym jakodevDependencies, - pakiety aplikacyjne dodają tylko to, co faktycznie jest im potrzebne jako runtime dependency,
- wspólne narzędzia (np.
jest,eslint,typescript) zaciągane są z root’a i konfigurowane przez core config.
pnpm automatycznie rozwiązuje, skąd brać paczkę, ale spójny podział między root a poszczególne pakiety upraszcza debugowanie i aktualizacje.
Skrypty z root’a kontra skrypty z pakietów
W każdej większej organizacji pojawia się pytanie: gdzie trzymać skrypty – w package.json root’a czy w pakietach? Rozsądny kompromis wygląda często tak:
- w pakietach definiowane są lokalne skrypty, specyficzne dla danego projektu (np.
build,dev), - w root’cie istnieją skrypty orkiestrujące, korzystające z
pnpm -r, Nx lub Turbo (np.pnpm test:all,pnpm lint:affected).
Przy wielu aplikacjach pomaga np. konwencja: pnpm dev --filter @org/admin uruchamia development tylko dla panelu admina, a analogicznie dla innych aplikacji. pnpm udostępnia flagę --filter, która stała się de facto standardem selekcji pakietów w monorepo.
Dodanie Nx do istniejącego pnpm monorepo
Strategia: Nx jako cienka warstwa nad istniejącymi pakietami
Najlepiej wprowadzać Nx tak, aby nie wywracać obecnego świata do góry nogami. Monorepo korzystające już z pnpm i działających skryptów nie musi od razu przechodzić na pełny „nx-only workflow”. Często wystarczy cienka warstwa:
- zdefiniowanie projektów Nx odpowiadających istniejącym pakietom,
- skonfigurowanie dependency graph i targets, które i tak już istnieją jako skrypty,
- stopniowe przenoszenie wybranych zadań do Nx (np. testów, lintów).
Taki tryb pozwala zespołowi oswoić się z Nx bez zrywania dotychczasowych nawyków. Jeżeli jakiś pakiet ma własne, nietypowe skrypty, Nx może je po prostu wywoływać, zamiast próbować je przepisać.
Instalacja Nx z pnpm
Pierwszy krok to dodanie Nx do monorepo. W katalogu głównym:
pnpm add -D nxNie jest konieczne użycie globalnego CLI – komendy Nx można uruchamiać przez pnpm nx <komenda>. To często wygodniejsze w zespołach, gdzie nie wszyscy mają globalnie zainstalowane narzędzia.
Podstawowy plik konfiguracyjny Nx
Nowsze wersje Nx korzystają z pliku nx.json w katalogu głównym. Prosty, ręcznie przygotowany start może wyglądać tak:
Minimalny nx.json dopasowany do pnpm workspaces
Podstawowa konfiguracja Nx nie musi być rozbudowana. Najpierw chodzi o to, by Nx „zobaczył” istniejące pakiety i potrafił dla nich uruchamiać zadania. Przykładowy nx.json dla monorepo z apps/ i packages/ może wyglądać tak:
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"npmScope": "org",
"workspaceLayout": {
"appsDir": "apps",
"libsDir": "packages"
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"test": {
"dependsOn": ["^build"],
"cache": true
},
"lint": {
"cache": true
}
}
}
Kilka elementów jest kluczowych:
npmScope– odpowiada prefiksowi paczek (@org/...),workspaceLayout– mówi Nx, gdzie szukać aplikacji i bibliotek,targetDefaults– nadaje domyślne zachowanie dla typowych zadań (cache, zależności).
Na tym etapie Nx nie zna jeszcze konkretnych projektów; trzeba je zdefiniować ręcznie lub wygenerować.
Ręczne definiowanie projektów Nx dla istniejących pakietów
Nx przechowuje informacje o projektach w plikach konfiguracyjnych obok pakietów. Dla prostoty można zacząć od jednego, reprezentatywnego projektu, np. apps/admin. W jego katalogu dodajemy plik project.json:
{
"name": "admin",
"sourceRoot": "apps/admin/src",
"projectType": "application",
"tags": ["type:app", "scope:frontend"],
"targets": {
"build": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm --filter @org/admin run build",
"cwd": ".",
"parallel": false
}
},
"test": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm --filter @org/admin run test",
"cwd": ".",
"parallel": false
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm --filter @org/admin run lint",
"cwd": ".",
"parallel": false
}
}
}
}
Intuicja jest prosta: Nx nie przejmuje kontroli nad budowaniem projektu, tylko wywołuje istniejące skrypty pnpm. Dzięki temu migracja jest niemal bezbolesna – jeśli ktoś woli nadal wpisywać pnpm --filter @org/admin test, nie ma konfliktu.
Kopia takiego project.json w innych katalogach (apps/api, packages/ui itd.) pozwoli Nx „objąć” kolejne pakiety, bez ruszania ich wnętrza.
Automatyczne wykrywanie projektów Nx z pnpm-lock.yaml
Przy większej liczbie pakietów ręczne dodawanie project.json do każdego jest uciążliwe. Nx ma mechanizmy automatycznej detekcji, korzystające z lockfile i struktury workspaces. Podstawą jest sekcja projects w nx.json. Najprostsza wersja może wyglądać tak:
{
"projects": {
"admin": "apps/admin",
"api": "apps/api",
"ui": "packages/ui",
"domain-core": "packages/domain-core"
}
}
Taki zapis mówi Nx tylko, gdzie fizycznie leżą projekty. Zadania (targets) można wtedy trzymać w jednym, wspólnym pliku, np. tools/nx-presets/frontend-project.json, i w razie potrzeby rozszerzać o dodatkowe opcje. To rozwiązanie jest wygodne, gdy zestaw zadań jest podobny w wielu pakietach.
Tagi i reguły zależności w Nx dopasowane do core/feature
Gdy projekty są już widoczne dla Nx, można dodać prostą warstwę zasad: co może się od kogo zależeć. Tu właśnie przydają się wcześniej wymyślone kategorie (core vs feature, frontend vs backend). W nx.json pojawia się sekcja namedInputs i implicitDependencies, ale przede wszystkim – pluginsConfig lub workspaceLayout z tags w projektach.
Przykładowa konfiguracja reguł w nx.json może wyglądać tak:
{
"nxCloudAccessToken": "",
"implicitDependencies": {
"package.json": "*",
"pnpm-lock.yaml": "*",
"tsconfig.base.json": "*",
"eslint.config.js": "*"
},
"pluginsConfig": {},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
}
},
"generators": {},
"workspaceLayout": {
"appsDir": "apps",
"libsDir": "packages"
},
"affected": {
"defaultBase": "main"
},
"defaultProject": "admin",
"namedInputs": {
"default": ["{projectRoot}/**/*", "!{projectRoot}/dist/**/*"],
"production": ["default", "!{projectRoot}/**/*.spec.[jt]s?(x)"]
},
"extends": [],
"projects": {
"admin": {
"tags": ["type:app", "scope:frontend"]
},
"api": {
"tags": ["type:app", "scope:backend"]
},
"ui": {
"tags": ["type:lib", "scope:frontend", "layer:ui"]
},
"domain-core": {
"tags": ["type:lib", "scope:shared", "layer:domain"]
}
},
"nx": {
"workspaceRules": [
{
"sourceTag": "layer:ui",
"onlyDependOnLibsWithTags": ["layer:domain", "layer:ui"]
},
{
"sourceTag": "layer:domain",
"onlyDependOnLibsWithTags": ["layer:domain"]
}
]
}
}
Reguły w sekcji nx.workspaceRules (lub w starszych wersjach lint.workspaceRules) pilnują, by np. warstwa UI nie zaczęła zależeć bezpośrednio od „losowych” bibliotek infrastrukturalnych. Przy kilkunastu osobach w zespole to często ratuje architekturę przed powolnym rozjechaniem się.
Uruchamianie istniejących skryptów przez Nx: pierwsze komendy
Gdy konfiguracja jest na miejscu, można sprawdzić, czy Nx poprawnie rozumie projekty. Kilka podstawowych komend:
pnpm nx graph– wizualizacja zależności między projektami; szybko widać, które pakiety są „w centrum”,pnpm nx run admin:build– wywołuje targetbuildzdefiniowany dla projektuadmin,pnpm nx run-many -t test --all– uruchamia testy we wszystkich projektach, z cache’owaniem.
W praktyce zespoły często dodają krótkie aliasy do package.json w root’cie, np. "graph": "nx graph", "test:affected": "nx affected -t test". To drobiazg, ale zmniejsza opór przed sięganiem po Nx na co dzień.
Integracja Nx z pnpm: wykorzystanie filter + affected
pnpm i Nx mają podobną ideę: uruchamiać tylko to, co faktycznie jest potrzebne. pnpm robi to za pomocą --filter, Nx – za pomocą mechanizmu „affected” (dotknięte zmianą). Połączenie tych dwóch często daje najlepsze efekty.
Przykładowy scenariusz dla testów:
- Nx oblicza, które projekty są „affected” w stosunku do gałęzi bazowej, np.
main. - Dla każdego takiego projektu uruchamia skrypt testów przez pnpm
--filter.
Konkretny rootowy skrypt może wyglądać tak:
{
"scripts": {
"test:affected": "nx affected -t test",
"lint:affected": "nx affected -t lint",
"build:affected": "nx affected -t build"
}
}
W project.json każdego projektu target test i tak korzysta z pnpm --filter, więc Nx staje się planistą, a pnpm – wykonawcą. Dodatkowo Nx korzysta z cache (lokalnego lub zdalnego), a pnpm – z własnego cache zależności. Jedno nie wyklucza drugiego.
Dołożenie Turborepo jako silnika zadań
Skoro monorepo ma już pnpm oraz Nx, po co jeszcze Turborepo? Jego silną stroną jest prosty, bardzo szybki system cache’owania i pipeline’ów, który dobrze współgra z projektami typu frontend+SSR. Nx z kolei świetnie zarządza grafem zależności i architekturą. Da się to pożenić.
Minimalny krok to dołożenie Turbo jako narzędzia do równoległego uruchamiania zadań, przy zachowaniu Nx jako „mózgu”, który mówi, jakie projekty są do ruszenia. Instalacja w root’cie:
pnpm add -D turboNastępnie plik turbo.json w katalogu głównym:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Tutaj Turbo nic nie wie o pnpm workspaces ani Nx. Tylko o tym, że hasło „build” jest ważne, ma zależność w górę grafu (^build), a jego efekt ląduje w dist/ czy build/. Cała reszta dzieje się w skryptach pakietów.
Wspólne skrypty orchestrujące Nx i Turborepo
Żeby narzędzia nie walczyły ze sobą, warto rozdzielić im role. Praktyczny podział:
- Nx – wylicza „które projekty” i pilnuje zasad architektonicznych,
- Turbo – optymalizuje „jak szybko” wykonać zadania (cache+równoległość),
- pnpm – rozprowadza komendy do konkretnych pakietów.
Przykładowy rootowy skrypt wykorzystujący Nx do selekcji i Turbo jako silnik:
{
"scripts": {
"ci:build": "nx print-affected --select=projects --type=app | xargs pnpm turbo run build --filter",
"ci:test": "nx print-affected --select=projects | xargs pnpm turbo run test --filter"
}
}
Mechanizm jest następujący: nx print-affected zwraca listę projektów, a Turbo, poprzez --filter, uruchamia zdefiniowane pipeline’y tylko dla tej grupy. W praktyce bywa potrzebne małe opakowanie w postaci skryptu Node’a lub prostego skryptu bash, żeby poprawnie zmapować nazwy Nx na filtry Turbo (np. @org/admin vs admin).
turbo.json a nx.json – jak nie dublować konfiguracji
Największe ryzyko przy łączeniu Nx i Turbo to zduplikowanie tej samej wiedzy o repo w dwóch miejscach. Dlatego konfiguracje warto potraktować asymetrycznie:
nx.json– prawda o architekturze i zależnościach: tagi, reguły, layout, projekty,turbo.json– opis pipeline’ów technicznych: jakie taski istnieją, co produkują, kiedy się invalidują.
Jeżeli w nx.json ustalone jest już, że build zależy od ^build, nie ma potrzeby kopiować tych samych reguł dosłownie do Turbo. Część zespołów ogranicza turbo.json tylko do definicji outputs i ewentualnych aliasów (np. "check": {"dependsOn": ["lint", "test"]}), zostawiając zarządzanie grafem Nx.
Konfiguracja pnpm + Turbo: filters i outputs
Turbo integruje się z pnpm głównie przez flagę --filter i rozpoznawanie pakietów z pnpm-workspace.yaml. Konkretny przykład komendy CI, bez Nx, dla pełnego zestawu testów:
pnpm turbo run test --filter=./apps/* --filter=./packages/*W połączeniu z outputs z turbo.json otrzymujemy cache na poziomie zadań. Gdy w packages/ui nie zmieni się żaden plik, Turbo nie uruchomi ponownie jego builda ani testów, korzystając z wyników poprzedniego przebiegu.
Dla wygody można dodać w root’cie aliasy skryptów:
{
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint"
}
}
W pakietach nadal znajdują się klasyczne skrypty "build", "test", "lint", natomiast Turbo w roli „wspólnego kierownika budowy” sprawia, że są wykonywane równolegle, z cache’owaniem.
Przenoszenie pojedynczych zadań z pnpm do Nx
Na początku Nx wywołuje tylko to, co już istnieje jako skrypty. Z czasem część zadań opłaca się przeprowadzić w pełni do Nx, szczególnie takich, które korzystają z informacji o grafie projektów. Dobrym kandydatem są np. generatory kodu, migracje czy bardziej złożone buildy.
Przykładowa ewolucja zadania testów:
- Start:
"test": "jest"wpackage.jsonpakietu, Nx wywołuje ten skrypt przeznx:run-commands. - Drugi krok: dodanie własnego executora Nx, który odpala Jest i wykorzystuje
inputs/outputsNx do precyzyjnego cache’owania.
Najczęściej zadawane pytania (FAQ)
Czym różni się monorepo od multi‑repo w praktyce?
W multi‑repo każdy projekt ma osobne repozytorium: osobny backend, frontend, design system i narzędzia. Na początku daje to poczucie porządku, ale szybko wychodzą problemy: różne wersje tych samych bibliotek, powielone konfiguracje, trudne zmiany przekrojowe wymagające wielu PR‑ów oraz gorszy onboarding nowych osób.
Monorepo zbiera całość kodu jednego produktu lub organizacji w jednym repozytorium. Dzięki temu łatwiej utrzymać jedną wersję bibliotek, jedną konfigurację narzędzi, uspójnić CI/CD i zrobić jedną gałąź z refaktoryzacją obejmującą frontend, backend i biblioteki. Zmiany między pakietami robisz lokalnie, bez publikowania ich do zewnętrznego rejestru.
Kiedy monorepo ma sens i dla jakich zespołów się opłaca?
Monorepo najbardziej pomaga tam, gdzie jest dużo współdzielonego kodu i częste zmiany przekrojowe – np. wspólny design system, biblioteki domenowe, kontrakty API między wieloma aplikacjami. Jeśli jedna zmiana w logice płatności dotyka panelu admina, publicznego frontu i API, monorepo znacząco upraszcza pracę.
Opłaca się szczególnie średnim i większym zespołom, które rozwijają kilka aplikacji wokół jednego produktu lub domeny biznesowej. W małych projektach z jedną apką korzyści są mniejsze, ale gdy tylko zaczynasz dodawać kolejne serwisy, monorepo szybko zwraca się w postaci spójniejszej architektury i łatwiejszego utrzymania.
Po co łączyć Nx, Turborepo i pnpm workspaces zamiast wybrać jedno narzędzie?
Każde z tych narzędzi rozwiązuje inny kawałek układanki. pnpm workspaces zajmuje się wyłącznie dependency managementem: definiuje pakiety, instaluje zależności i linkuje lokalne paczki. Nx rozumie architekturę projektu: buduje graf zależności, ma komendy affected:<target>, reguły architektoniczne i executory do budowania, testów czy lintowania. Turborepo jest lekkim silnikiem pipeline’ów z agresywnym cache’owaniem lokalnym i zdalnym.
Łącząc je, dostajesz solidny fundament dependency (pnpm), „mózg” rozumiejący relacje między pakietami (Nx) i bardzo szybki, prosty w konfiguracji pipeline (Turbo), np. dla CI. Taki zestaw ma sens w większych organizacjach, które chcą jednocześnie kontroli architektury i maksymalnej szybkości buildów, bez rezygnowania z zalet pnpm workspaces.
Czy mogę używać tylko pnpm workspaces bez Nx i Turborepo w monorepo?
Tak, technicznie możesz postawić monorepo tylko na pnpm workspaces i kilku własnych skryptach npm. pnpm świetnie radzi sobie z instalacją i linkowaniem zależności między pakietami, więc do prostych układów to bywa wystarczające.
Problem zaczyna się, gdy rośnie liczba pakietów i aplikacji. pnpm nie wie nic o tym, że pakiet A architektonicznie zależy od pakietu B, nie policzy też, które projekty są „dotknięte” konkretną zmianą w kodzie. Wtedy wchodzi Nx z dependency graphem i komendami affected, a Turborepo może przejąć rolę łatwego w utrzymaniu silnika pipeline’ów z mocnym cache’owaniem.
Jak ułożyć strukturę katalogów w monorepo z Nx i pnpm workspaces?
Najczęstszy wzorzec to rozdzielenie aplikacji i bibliotek, np.:
apps/admin,apps/landing,apps/api– samodzielne aplikacje, które są uruchamiane przez użytkownika lub system,packages/ui,packages/utils,packages/payments– biblioteki z kodem współdzielonym.
Inny popularny wariant to samo packages/, gdzie zarówno apki, jak i biblioteki są „pakietami”, np. packages/admin-app, packages/ui. W stylu Nx często spotyka się apps/ i libs/; w pnpm workspaces można to łatwo odwzorować, definiując wzorce typu apps/* i libs/*. Ważne, żeby zespół jasno rozumiał, co jest apką, a co biblioteką.
Jak Nx i Turborepo przyspieszają CI/CD w monorepo?
Nx potrafi obliczyć, które projekty są faktycznie dotknięte daną zmianą (komendy nx affected:<target>). Dzięki dependency graphowi nie musisz budować i testować całego monorepo – uruchamiasz tylko zadania dla aplikacji i bibliotek zależnych od zmienionego kodu, np. odświeżonego design systemu.
Turborepo dodaje do tego bardzo agresywne cache’owanie wyników zadań: build, test, lint. Jeśli kod i zależności się nie zmieniły, pipeline może pominąć część kroków, korzystając z cache’u lokalnego lub zdalnego (np. w CI). W praktyce oznacza to krótsze kolejki w CI, szybsze feedbacki po PR‑ach i mniej „czekania na build”, zwłaszcza w dużych repo z wieloma aplikacjami.
Czy monorepo nie skończy się „spaghetti w jednym repozytorium”?
Może, jeśli zabraknie przemyślanej architektury pakietów i reguł zależności. Monorepo nie jest automatycznie lepsze – daje tylko inne narzędzia. Bez podziału na apki, biblioteki i narzędzia, oraz bez kontroli kto może zależeć od kogo, szybko powstaje trudna do ogarnięcia sieć zależności.
Nx pomaga temu zapobiegać, bo wymusza myślenie o projektach (apps, libs) i relacjach między nimi. Możesz definiować, które segmenty repo mogą się „widzieć”, a dependency graph od razu pokaże, gdy biblioteka z niskiego poziomu zaczyna niebezpiecznie zależeć od „wysokopoziomowej” apki. Samo narzędzie jednak nie wystarczy – potrzebna jest jeszcze dyscyplina w zespole i podstawowe zasady projektowania modułów.
Co warto zapamiętać
- Monorepo rozwiązuje typowe problemy multi‑repo: rozjeżdżające się wersje bibliotek, duplikację konfiguracji, trudne zmiany przekrojowe i uciążliwy onboarding, bo wszystko ląduje w jednym repo i jednej przestrzeni zależności.
- Wspólny kod – design system, utilsy, typy, kontrakty API – jest „pod ręką”, nie trzeba go pakować i publikować do zewnętrznego rejestru, dzięki czemu refaktoryzacje i poprawki rozchodzą się szybciej i bez ręcznego pilnowania wersji.
- Monorepo daje jedną wersję prawdy: spójne wersje bibliotek, wspólne konfiguracje narzędzi (ESLint, Prettier, tsconfig) i jednolite skrypty build/test/lint, co porządkuje procesy w całym zespole.
- Połączenie pnpm, Nx i Turborepo wykorzystuje mocne strony każdego narzędzia: pnpm zarządza zależnościami i workspace’ami, Nx pilnuje architektury i dependency graphu, a Turborepo zapewnia prosty, szybki pipeline z agresywnym cache’owaniem.
- Same pnpm workspaces nie znają powiązań architektonicznych między pakietami, Nx bywa złożony konfiguracyjnie, a Turborepo nie narzuca struktury – dopiero ich kombinacja daje zarówno porządek architektoniczny, jak i lekki, zrozumiały pipeline CI/CD.
- W praktycznym scenariuszu z wieloma aplikacjami (np. admin, landing, API, design system) monorepo umożliwia lokalne współdzielenie pakietów, uruchamianie tylko potrzebnych buildów i testów (Nx affected) oraz znaczące przyspieszenie CI dzięki cache’owi Turborepo.






