Back to blog
Published 27 May 20255 min read
Monorepos with Nx and Turborepo: what I kept and what I dropped
After several products in Nx and Turborepo monorepos: the structure, caching and shared-type habits that paid off, and the ones that only added ceremony.
MonorepoNxTurborepoTypeScript
Why a monorepo at all
Most of the products I worked on had a web app, an admin app, sometimes a mobile app, and one or more backend services. Without a monorepo, the same types, validation rules and UI components were copied between repositories and slowly drifted apart.
What I kept
- Shared types and validation in one package. API request and response shapes defined once, with Zod schemas that both the backend and the frontend import.
- A shared UI package with Storybook, so a button or a table is fixed once for every app.
- Remote caching for builds and tests. On a busy branch it turns a ten-minute pipeline into a two-minute one.
- One lint and format setup for the whole repository.
apps/
web/ // Next.js
admin/ // React
api/ // NestJS
packages/
shared-types/ // Zod schemas + inferred types
ui/ // components + StorybookWhat I dropped
Generators for every new file, deep folder hierarchies inside packages, and packages created "just in case". A package earns its place when two apps really use it.
The rule that matters most
Dependencies point one way: apps depend on packages, packages never depend on apps. Enforce it with lint rules; the day it breaks, builds start to fail in confusing ways.