Werk je aan meerdere Node.js projecten die code delen, dezelfde types nodig hebben of samen uitgerold worden? Dan is een monorepo architectuur vaak de oplossing. In plaats van tien losse repo's met duplicatie van utilities, types en configuratie, zet je alles bij elkaar in één repository met duidelijke grenzen tussen packages.
In deze gids leggen we uit wat een monorepo is, wanneer je er één moet gebruiken, hoe je er eentje opzet met npm workspaces en Turborepo, en welke valkuilen je wilt vermijden.
Wat is een monorepo precies?
Een monorepo (monolithic repository) is één Git-repository die meerdere losse projecten bevat. Die projecten kunnen applicaties zijn (een API, een frontend, een worker) of herbruikbare libraries (shared types, utilities, UI-componenten).
Binnen zo'n repo heeft elk project zijn eigen package.json, eigen tests en eigen build. Maar ze delen de Git-geschiedenis, de CI-configuratie en vaak ook linting, TypeScript-instellingen en dependencies.
Dit staat tegenover de polyrepo aanpak, waar elk project in zijn eigen repository leeft. Beide hebben voor- en nadelen, het is geen zwart-wit keuze.
Waarom kiezen voor een monorepo?
De grootste winst zit in code delen zonder publiceren. Heb je een API en een frontend die dezelfde TypeScript-types gebruiken voor je REST-endpoints? In een polyrepo moet je die types publiceren naar een private npm-registry en versies bijhouden. In een monorepo importeer je ze direct.
Andere voordelen:
- Atomic commits: één wijziging in de shared package én alle consumers in dezelfde commit
- Consistente tooling: één versie van ESLint, Prettier, TypeScript voor alles
- Eenvoudiger refactoren: een rename in een shared type werkt meteen door
- Gedeelde CI: één pipeline voor alles, met slimme caching
Populaire bedrijven zoals Google, Meta en Microsoft draaien enorme monorepo's. Maar ook kleinere teams hebben er baat bij.
Wanneer is een monorepo een slecht idee?
Niet elk project heeft een monorepo nodig. Bouw je één Node.js API zonder shared code? Blijf dan bij een normale repo.
Een monorepo voegt complexiteit toe:
- Tooling moet workspaces begrijpen
- CI wordt ingewikkelder (je wil niet alles opnieuw bouwen bij elke commit)
- Toegangsrechten scheiden is lastiger
- De repo kan groot en traag worden
Als je één op zichzelf staand project hebt, of projecten die echt niets delen, dan is een polyrepo eenvoudiger. Zie ook onze gids over modules en project structuur in Node.js voor een enkelvoudige projectopbouw.
npm workspaces: de basis
Sinds npm 7 zijn workspaces ingebouwd. Je hebt geen externe tool nodig om meerdere packages in één repo te beheren.
Projectstructuur
Een typische workspace-indeling:
my-monorepo/
├── package.json
├── apps/
│ ├── api/
│ │ └── package.json
│ └── web/
│ └── package.json
└── packages/
├── shared/
│ └── package.json
└── config/
└── package.json
Root package.json
In de root configureer je workspaces:
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
]
}
De private: true voorkomt dat je per ongeluk de root publiceert. De glob patterns vertellen npm welke mappen workspaces bevatten.
Package linken
Wil je in apps/api de packages/shared gebruiken? Geef de shared package een naam:
{
"name": "@my-org/shared",
"version": "1.0.0",
"main": "./src/index.js"
}
En voeg hem toe als dependency in de API:
{
"dependencies": {
"@my-org/shared": "*"
}
}
Na een npm install in de root linkt npm de shared package via symlinks. Je kunt hem nu gewoon importeren:
import { formatDate } from '@my-org/shared'
Wijzigingen in packages/shared zijn direct beschikbaar, geen publish nodig.
pnpm en yarn: alternatieven
Naast npm workspaces zijn pnpm en yarn populair. Ze werken conceptueel hetzelfde maar hebben eigen voordelen.
pnpm is vaak de favoriet voor grote monorepo's. Het gebruikt een content-addressable store, waardoor dependencies maar één keer op je schijf staan. Dat scheelt gigabytes en maakt installs sneller. Ook is pnpm strenger met hoisting, wat phantom dependencies voorkomt.
yarn (vooral yarn 3+ met PnP) biedt vergelijkbare features en is een goede keuze als je team er al mee werkt.
Welke je kiest maakt minder uit dan consistent blijven. Onze voorkeur voor nieuwe monorepo's is pnpm vanwege de snelheid en strictness.
Turborepo: builds en caching
Workspaces regelen dependencies, maar niet builds en tests. Als je tien packages hebt en je past er één aan, wil je niet alle tien opnieuw bouwen.
Turborepo lost dit op. Het is een build-systeem dat:
- Een task graph opbouwt tussen packages
- Alleen packages opnieuw bouwt waarin iets veranderde
- Output cached lokaal én remote (in bijvoorbeeld Vercel of S3)
- Taken parallel uitvoert waar mogelijk
Minimale configuratie
Installeer Turborepo en maak een turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
dependsOn: ["^build"] betekent: eerst build van alle dependencies draaien. outputs vertelt Turbo wat het moet cachen.
Vanaf de root draai je dan:
turbo run build
turbo run test --filter=@my-org/api
Die tweede draait alleen de tests voor de API-package én alles waar die van afhangt.
Remote caching
De killer-feature is remote caching. Als collega A een build doet, kan collega B (en de CI) die build hergebruiken. Dat scheelt minuten per pipeline. Voor teams die meerdere keren per dag mergen is dit essentieel.
Nx: een ander ecosysteem
Nx is het grote alternatief voor Turborepo. Het biedt vergelijkbare caching en task-running, maar gaat verder met:
- Generators voor nieuwe packages
- Dependency graph visualisatie
- Affected commands (draai alleen tests voor gewijzigde code)
- Plugins voor React, Next.js, NestJS en meer
Nx is zwaarder en meer "opinionated". Turborepo is minimaler en laat je zelf kiezen. Voor Node.js monorepo's met veel TypeScript is Nx een sterke kandidaat, zeker als je ook frontends deelt.
Shared packages: wat zet je erin?
Een veelgemaakte fout is alles delen. Je monorepo is geen reden om elke utility in een shared package te proppen. Begin klein en deel alleen wat écht gedeeld wordt.
Goede kandidaten voor shared packages:
- Types: REST API contracts, database modellen, event schemas
- Validatie: Zod-schemas die frontend én backend gebruiken (zie onze gids over Zod)
- Configuratie: gedeelde ESLint, TypeScript, Prettier configs
- Utilities: date formatting, currency helpers, constants
- Logging en observability: één logger-setup voor alle services
Slechte kandidaten:
- Business logic die eigenlijk bij één service hoort
- Code die maar door één consumer wordt gebruikt
- Te brede "utils" packages waar alles op een hoop ligt
TypeScript in een monorepo
TypeScript heeft eersteklas support voor monorepo's via project references. Je root tsconfig.json verwijst naar sub-projecten, en elke package heeft zijn eigen tsconfig.json die erft van een gedeelde base.
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../shared" }
]
}
Met tsc --build bouwt TypeScript packages in de juiste volgorde en skipt wat al up-to-date is. Combineer dit met Turborepo's caching en je builds blijven snel, ook bij tientallen packages.
Gebruik je strict types over je hele stack, dan wordt refactoren een stuk veiliger, verander een type in shared en de TypeScript compiler wijst alle plekken aan waar het breekt.
CI/CD voor een monorepo
Je CI-pipeline moet slim worden. Bij elke commit alles opnieuw bouwen en testen schaalt niet. Twee strategieën:
1. Affected detection: tools als Turborepo en Nx kunnen bepalen welke packages veranderden en alleen die (plus hun dependents) testen.
turbo run test --filter=...[origin/main]
Dit draait tests voor alles wat veranderde sinds de main branch.
2. Remote caching: cache build-output op een centrale plek. Als een job dezelfde inputs heeft als een eerdere, hergebruik dan het resultaat.
Combineer dit met de principes uit onze performance tuning gids voor Node.js om pipeline-tijden onder controle te houden.
Deploys vanuit een monorepo
Eén veel gestelde vraag: hoe deploy je een enkele service vanuit een monorepo zonder alle andere code mee te sturen?
Opties:
- Docker multi-stage builds: kopieer alleen de relevante package en zijn dependencies
- Turborepo prune:
turbo prune --scope=@my-org/apigenereert een minimale sub-repo met alleen wat nodig is - Monorepo-aware platforms: Vercel, Render en Railway detecteren workspaces automatisch
Voor containerized deploys in combinatie met achtergrondprocessen werkt dit goed samen met de patronen uit onze background jobs en workers gids.
Best practices
Een paar richtlijnen uit de praktijk:
- Hou packages klein en gefocust: elk met een duidelijke verantwoordelijkheid
- Gebruik een naming convention:
@my-org/*voorkomt conflicten en maakt de ownership duidelijk - Pin dependency versies in de root waar mogelijk, zodat alle packages dezelfde versies gebruiken
- Documenteer de repo-structuur in een README, nieuwe teamleden moeten snel hun weg vinden
- Gebruik Changesets voor versiebeheer als je packages publiceert naar npm
- Scheid apps van libraries via duidelijke mappen zoals
apps/enpackages/ - Monitor de groei: bij honderden packages heb je mogelijk een tool als Nx nodig in plaats van pure workspaces
Veelgestelde vragen
Wat is een monorepo?
Een monorepo is een enkele Git-repository die meerdere projecten of packages bevat. In plaats van elke service of library in een eigen repo te plaatsen, leven ze naast elkaar en delen ze tooling, configuratie en afhankelijkheden.
Wanneer moet ik een monorepo gebruiken?
Een monorepo is handig als je meerdere gerelateerde projecten hebt die code delen, zoals een frontend, backend en shared utilities. Ook bij microservices met veel overlap werkt het goed. Voor één op zichzelf staand project is het overkill.
Wat is het verschil tussen npm workspaces en Turborepo?
npm workspaces is een ingebouwde feature voor dependency management binnen één repo. Turborepo is een build-systeem dat bovenop workspaces draait en taken cached, parallelliseert en alleen veranderde packages opnieuw bouwt.
Wat zijn de nadelen van een monorepo?
Een monorepo kan groot en traag worden bij veel code. Tooling is complexer, CI-pipelines moeten slim gebouwd worden en toegangsrechten zijn lastiger te scheiden. Zonder goede caching worden builds snel een knelpunt.
Hoe deel je code tussen packages in een monorepo?
Je maakt een shared package aan (bijvoorbeeld in packages/shared) en importeert die vanuit andere packages via de workspace-naam. De package manager linkt deze lokaal, zodat je wijzigingen direct beschikbaar zijn zonder te publiceren.
Conclusie
Een monorepo architectuur is geen silver bullet, maar voor teams met meerdere Node.js projecten die code delen is het vaak de juiste keuze. Je krijgt consistente tooling, atomic commits en eenvoudig shared code, zonder het publiceer-circus van private npm-packages.
Begin simpel met npm of pnpm workspaces. Voeg Turborepo of Nx toe zodra builds trager worden. En hou je packages klein en gefocust, een monorepo is geen excuus om alles op één hoop te gooien.