Frontend Monorepos Explained: A Beginner’s Guide with Turborepo and PNPM
A practical beginner-friendly guide to understanding monorepos, sharing code, organizing projects, and simplifying frontend development.
Introduction
As frontend applications grow, developers often need to manage multiple projects at the same time.
For example, a company may have a marketing website, an admin dashboard, a customer portal, and a shared component library. Managing all of these in separate repositories can become difficult, especially when they use similar components, utilities, and configurations.
Imagine updating the same Button component in three different projects. You would need to make the same changes in multiple places.
This is where a monorepo can help.
A monorepo allows developers to manage multiple related projects inside a single Git repository. It also makes sharing code and maintaining consistency easier.
In this article, we’ll explore what a monorepo is, why frontend teams use it, and how to create one using PNPM Workspaces and Turborepo. We’ll also walk through a practical example to understand how everything works together.
What Is a Monorepo?
A monorepo (short for “monolithic repository”) is a single Git repository that contains multiple projects.
Instead of creating different repositories for each application, everything is stored in one place.
Example
Without a monorepo:
website-repo/
admin-dashboard-repo/
customer-portal-repo/
shared-ui-repo/Each project has its own repository. If multiple projects need the same component or utility, developers may have to maintain separate copies or publish and install shared packages.
With a monorepo:
company-projects/
│
├── apps/
│ ├── website/
│ ├── admin-dashboard/
│ └── customer-portal/
│
├── packages/
│ ├── ui/
│ └── shared-utils/
│
└── package.jsonNow, all the applications and shared packages live inside one repository.
Each application can still have its own dependencies, development server, and deployment process. They simply share the same repository.
A Simple Real-Life Analogy
Imagine a family living in one house.
- Each room has a different purpose.
- The kitchen is used for cooking.
- The bedroom is used for sleeping.
- The living room is used for relaxing.
Although every room has a different job, they all belong to the same house.
A monorepo works in a similar way.
Different applications and libraries have different purposes, but they are all stored in the same repository.
Why Do Teams Use Monorepos?
Many teams use monorepos because they make managing related projects easier.
Let’s take a practical example.
Imagine a company has two applications:
- A website where visitors can explore products and services.
- An admin dashboard where employees manage users, products, and orders.
Both applications may need similar UI components, such as buttons, input fields, dropdowns, modals, and tables.
Instead of creating these components separately in both applications, developers can create a shared UI package and use it wherever needed.
This helps reduce duplicated code and makes future updates easier.
Here are some common benefits of using a monorepo.
Benefits of a Monorepo
1. Code Reuse
One of the biggest benefits is code sharing.
Instead of copying the same code into different applications, you can create a shared package.
For example:
packages/
└── ui/
└── src/
└── components/
├── Button.tsx
├── Input.tsx
├── Select.tsx
└── Modal.tsxNow, both the website and admin dashboard can use components from the same UI package.
If you need to update the Button component, you can update it in one place instead of maintaining separate versions.
2. Easier Maintenance
When shared code changes, developers only need to update it in one place.
Without a monorepo, the same update might have to be made in several repositories.
With a monorepo:
- Shared code stays in one place.
- Changes are easier to manage.
- Developers can review related changes together.
3. Better Collaboration
Frontend developers often work on multiple applications within the same organization.
Since related projects are stored in one repository:
- Developers can easily explore related applications.
- Pull requests can include changes across multiple projects.
- Dependencies between applications and packages are easier to understand.
4. Consistent Tooling
All projects can share common development configurations, such as:
- ESLint rules
- Prettier configuration
- TypeScript settings
- Testing tools
This helps maintain similar coding standards across different applications.
5. Easier Refactoring
Imagine you want to rename a shared component or change the structure of a common utility function.
In a monorepo, you can update the shared package and review how that change affects the applications using it.
This makes large refactoring tasks easier to coordinate.
Challenges of Monorepos
Monorepos are useful, but they are not perfect.
1. Bigger Repository Size
As more applications and packages are added, the repository can become larger.
This may increase cloning time and make repository management more challenging.
2. More Complex Configuration
Setting up a monorepo requires additional configuration.
Developers need to understand how applications, shared packages, dependencies, and build tasks work together.
3. Longer Build Times
If a repository contains multiple applications, running every build task unnecessarily can take more time.
This is one reason tools like Turborepo are useful. They help manage task execution and reuse cached results.
4. Learning Curve
Developers who are new to monorepos may need time to understand:
- Workspaces
- Shared packages
- Dependency management
- Task pipelines
- Build caching
However, once you understand the basic concepts, managing a monorepo becomes much easier.
Popular Monorepo Tools
There are several tools available for managing monorepos.
1. Turborepo
<Turborepo> is a high-performance build system for JavaScript and TypeScript monorepos.
It helps manage tasks such as building, testing, linting, and running applications.
Some important features include:
- Build caching
- Task scheduling
- Dependency management between tasks
- Running tasks across multiple applications
- Filtering tasks for specific projects
We’ll use Turborepo in our practical example later in this article.
2. Nx
Nx is another popular build system for monorepos.
It provides features such as:
- Project generators
- Dependency graph visualization
- Build optimization
- Task caching
- Support for large development teams
3. PNPM Workspaces
PNPM is a fast and disk-efficient package manager.
Its workspace feature allows developers to manage multiple packages inside one repository.
It helps connect local applications and shared packages without needing to publish them separately.
One important thing to understand: PNPM Workspaces and Turborepo are not alternatives to each other.
PNPM manages dependencies and workspace packages, while Turborepo manages tasks and caching. They can work together in the same project.
Understanding a Typical Frontend Monorepo Structure
Before creating a monorepo, let’s understand how its folders are usually organized.
For our example, imagine we’re building a platform with a website and an admin dashboard.
We can organize the repository like this:
my-monorepo/
│
├── apps/
│ ├── website/
│ │ ├── src/
│ │ └── package.json
│ │
│ └── admin-dashboard/
│ ├── src/
│ └── package.json
│
├── packages/
│ ├── ui/
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Input.tsx
│ │ │ │ ├── Select.tsx
│ │ │ │ └── Modal.tsx
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ ├── utils/
│ │ └── package.json
│ │
│ ├── eslint-config/
│ │ └── package.json
│ │
│ └── typescript-config/
│ └── package.json
│
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── pnpm-lock.yamlLet’s understand the main directories.
apps/
This directory contains the actual applications.
For example:
website/contains the public-facing website.admin-dashboard/contains the internal dashboard.
Each application can have its own pages, routes, layouts, and application-specific features.
packages/
This directory contains reusable code and configurations.
For example:
ui/contains reusable components such as Button, Input, Select, and Modal.utils/contains common helper functions.eslint-config/contains shared ESLint rules.typescript-config/contains shared TypeScript configurations.
Root configuration files
Get Manoj Belbase’s stories in your inbox
Join Medium for free to get updates from this writer.
There are also a few important files at the root:
package.jsonmanages root-level dependencies and scripts.pnpm-workspace.yamltells PNPM which directories contain workspace packages.turbo.jsondefines how Turborepo runs tasks.pnpm-lock.yamlrecords the resolved dependency versions.
This structure helps keep applications and shared packages organized.
Step-by-Step: Creating a Frontend Monorepo with PNPM and Turborepo
Now let’s create a simple monorepo using PNPM and Turborepo.
We’ll use the same example: a website, an admin dashboard, and a shared UI package.
Step 1: Create a Monorepo
First, make sure Node.js and PNPM are installed.
You can check their versions:
node -v
pnpm -vCreate a new Turborepo project:
pnpm dlx create-turbo@latest my-monorepoThis command creates a starter monorepo with the basic configuration and example projects.
Move into the project:
cd my-monorepoYou can explore the generated folders and files to understand how the workspace is organized.
Step 2: Understand PNPM Workspaces
Inside the root directory, you’ll find a file named:
pnpm-workspace.yaml
It tells PNPM which directories contain workspace packages.
For example:
packages:
- "apps/*"
- "packages/*"This configuration tells PNPM to look for packages inside the apps/ and packages/ directories.
Whenever you add a new application or shared package inside these directories, PNPM can include it in the workspace.
Step 3: Create Applications and Shared Packages
Inside apps/, we can have:
apps/
├── website/
└── admin-dashboard/Inside packages/, we can create:
packages/
└── ui/The website and admin dashboard are independent applications, while the UI package contains reusable components.
For example, the UI package might contain:
ui/
└── src/
└── components/
├── Button.tsx
├── Input.tsx
├── Select.tsx
└── Modal.tsxWe don’t need to write the same Button or Input component separately in every application.
Step 4: Connect the Shared UI Package
Now comes the interesting part.
How can the website and admin dashboard use components from the shared UI package?
First, the UI package needs a package name in its package.json.
For example:
{
"name": "@repo/ui",
"version": "1.0.0"
}Then, inside the website’s package.json, we can add the UI package as a dependency:
{
"dependencies": {
"@repo/ui": "workspace:*"
}
}We can do the same inside the admin dashboard.
The workspace:* protocol tells PNPM to use the local workspace package rather than looking for a separately published version.
After installing dependencies, the applications can import components exposed by the UI package.
For example:
import { Button } from "@repo/ui";Now the website and admin dashboard can use the same Button component.
If you update that component inside the shared UI package, both applications can receive the updated version.
That’s one of the main advantages of using a monorepo.
Note: The UI package also needs to expose its components through its package exports or entry point. Some framework and build configurations may require additional setup to consume shared TypeScript or TSX files.
Step 5: Understand Turborepo Configuration
Now that our applications and packages are connected, we need a way to manage their development tasks.
This is where Turborepo comes in.
At the root of the repository, you’ll find:
turbo.json
This file defines the tasks that Turborepo can run.
For example:
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {},
"check-types": {}
}
}Let’s understand the important properties.
dependsOn
This defines which tasks should run before the current task.
For example:
"dependsOn": ["^build"]The ^build means Turborepo should build the dependencies of a package before building that package itself.
Imagine the admin dashboard depends on the shared UI package. The shared UI package should be built first if the dashboard’s build depends on its output.
outputs
This tells Turborepo which generated files should be saved in the task cache.
For example, a Next.js application may generate files inside .next/, while another package may generate files inside dist/.
The correct output paths depend on your project.
cache
This controls whether Turborepo should cache a task’s results.
For example, development servers are long-running tasks, so we usually don’t want to cache them.
persistent
This tells Turborepo that a task keeps running, such as a development server.
These settings help Turborepo understand how to run tasks efficiently.
Step 6: Run Applications Using Turborepo
Now let’s see how we can run our applications.
Inside the root package.json, we can define scripts like these:
{
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"lint": "turbo lint"
}
}Now we can run commands from the root directory.
Start the development tasks:
pnpm devBuild the applications:
pnpm buildRun linting tasks:
pnpm lintTurborepo looks at the packages in the workspace and runs the tasks defined in their respective package.json files.
For example, if the website and admin dashboard both have a dev script, Turborepo can start both applications through the root command.
Step 7: Run a Specific Application
Sometimes, you don’t need to run every application.
Imagine you’re working only on the admin dashboard.
Instead of starting all the applications, you can use Turborepo’s filtering feature:
pnpm turbo dev --filter=admin-dashboardThis tells Turborepo to run the development task for the package named admin-dashboard.
This is especially useful when a monorepo contains many applications and packages.
What Is Caching in Turborepo?
Caching is one of the most useful features of Turborepo.
Imagine you build your admin dashboard. The build process takes some time.
Then, you make a small change in an unrelated part of your project and run the build command again.
Without caching, the build process might repeat work that has already been completed.
Turborepo helps avoid unnecessary work by storing the results of tasks.
When you run the same task again with the same relevant inputs and configuration, Turborepo can reuse the previous result instead of executing the task again.
This is called a cache hit.
For example:
First build:
admin-dashboard:build
Cache miss
Building application...Second build without relevant changes:
admin-dashboard:build
Cache hit
Reusing previous build output...This can significantly reduce repeated build work, especially in larger repositories.
When Should You Use a Monorepo?
A monorepo can be a good choice when:
- You manage multiple related applications.
- Several applications share components and utilities.
- You maintain a shared design system.
- Different teams work on related frontend projects.
- You want consistent tooling across applications.
For example, if a company has a website, admin dashboard, and customer portal that share the same UI components, a monorepo can make sense.
When Should You Avoid a Monorepo?
A monorepo may not be necessary when:
- You only have one small application.
- Projects are completely unrelated.
- There is very little shared code.
- Your team wants the simplest possible setup.
Remember, using a monorepo doesn’t automatically make a project better. Choose an approach based on your actual development requirements.
Conclusion
A monorepo is a way of managing multiple related projects inside a single Git repository.
It helps developers share code, maintain consistency, and collaborate across applications.
With PNPM Workspaces, you can manage dependencies and connect local packages. With Turborepo, you can manage development tasks, build dependencies in the correct order, and reuse cached results to avoid unnecessary work.
If you’re working with multiple React, Next.js, or other frontend applications that share components and utilities, learning about monorepos can be very useful.
You don’t need to understand everything at once. Start with a small project, create a couple of applications, add a shared UI package, and explore how PNPM and Turborepo work together.
The best way to understand a monorepo is to build one yourself.








