
Monorepos with Turborepo: Managing Multi-Package Projects
When a monorepo pays off, how Turborepo speeds up builds with caching and task graphs, and how to structure web apps, APIs and shared packages sensibly.
In short: A monorepo keeps several related projects, such as a web app, backend, admin interface and shared libraries, in a single Git repository. Turborepo is a build tool for JavaScript and TypeScript monorepos that runs tasks like build, test and lint in the right order and caches the results, so unchanged packages are not rebuilt. It pays off once several applications share code and changes across package boundaries should ship together.
What a monorepo is and what it solves
Many software projects consist of closely related parts: a Next.js website, a mobile app, an API, and packages with shared types, UI components or validation rules. When those parts live in separate repositories, friction builds up. Changing a shared data type means releasing the package, bumping the version in every dependent project and coordinating several pull requests.
In a monorepo the same change is one commit. The TypeScript compiler immediately shows which applications need updating, and everything is tested together. That is especially useful when frontend and backend share types, as we describe in our guide to full-stack development with TypeScript.
A monorepo is not a goal in itself, though. Without the right tooling every build gets slower, because each change rebuilds and retests everything. That is the problem Turborepo addresses.
How Turborepo works
Workspaces as the foundation
Turborepo does not replace your package manager. It builds on the workspaces of npm, pnpm, Yarn or Bun, which manage several packages in one repository and link them locally. A typical layout:
apps/
web/ # Next.js application
admin/ # internal dashboard
api/ # backend
packages/
ui/ # shared React components
types/ # shared TypeScript types
config/ # ESLint and TypeScript config
The task graph
In turbo.json you declare which tasks exist and what they depend on. "dependsOn": ["^build"], for example, means that before an app is built, every package it depends on must be built. Turborepo derives the order from this and runs everything independent in parallel.
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**"] },
"test": { "dependsOn": ["^build"] },
"lint": {}
}
}
Caching
The biggest time saving comes from caching. Turborepo computes a hash for each task from source code, dependencies and configuration. If nothing changed, it reuses the stored result instead of running the task again. With a remote cache, developers and CI servers share those results, so something built once on a laptop does not have to be built again in the pipeline.
Targeted runs
Filters restrict tasks to specific packages, for example turbo run build --filter=web. In CI you can check only the packages actually affected by a change. Our practical guide to CI/CD pipelines shows how to set that up.
Structuring a monorepo well
The split between apps and packages has proven itself. Apps get deployed, packages get used by apps. Each package should have a clear purpose and not turn into a dumping ground for anything that appears twice.
Shared configuration for ESLint, Prettier and TypeScript belongs in its own package, so every project follows the same rules and a change applies everywhere. Our clean code principles for TypeScript cover useful conventions.
A typical shared package is the UI library. If you are building a design system, its components live there and serve the website, admin interface and customer portal alike.
When Turborepo is the wrong choice
For a single project with no shared code, a monorepo only adds structure. When teams work fully independently with separate release cycles, separate repositories can also be the better fit.
Turborepo focuses on fast task execution and caching. If you also need code generators, strict rules about which packages may depend on which, or support for many languages, take a look at Nx. For very small setups, plain pnpm workspaces without an extra tool are sometimes enough.
Frequently asked questions (FAQ)
Doesn't a monorepo get messy with many packages?
Not if the structure is clear and each package has one job. With filters, each team member usually works on only a few packages anyway. Things get messier when the same logic is copied across several repositories.
Does Turborepo need a paid remote cache?
No. Local caching works without any extra service. For remote caching you can connect to Vercel or use a self-hosted server that implements the open cache API.
Can I move an existing project into a monorepo?
Yes. Usually you start by moving the existing repositories, with their Git history, into one repository and setting up workspaces. Turborepo can then be added step by step.
How are packages versioned in a monorepo?
Internal packages that are only used inside the repository often need no version of their own. For published packages, a tool such as Changesets helps maintain version numbers and changelogs.
Conclusion
A monorepo pays off when several applications share code and evolve together. Turborepo keeps that convenience from costing you long build times, provided the task graph and caching are configured properly from the start.
If you want to bring several applications onto a shared codebase, talk to us about web development.
Related posts
- React vs. Vue vs. Svelte: A Frontend Framework Comparison
React, Vue or Svelte for your next web project? A sober comparison of concepts, ecosystem, performance, hiring and long-term maintainability.
- Why we chose Next.js as our default web framework
Why we build web apps with Next.js by default: rendering options, SEO, TypeScript, self-hosting in Germany, and when we deliberately choose otherwise.
- Testing React Applications: Unit, Integration and E2E Tests
How to test React applications sensibly: which tests pay off, which tools fit, and how a test strategy catches bugs early instead of slowing the team down.
- Clean code in TypeScript: principles for maintainable projects
Practical clean code rules for TypeScript: strict types, clear names, small functions and validated boundaries, with examples and the mistakes to avoid.