Note: This post is a translated version of an article originally published on my personal blog. You can read the original Korean post here.
What is a Monorepo?
A monorepo is an approach where multiple projects are managed within a single repository. This helps maintain code consistency, improves reusability, and simplifies dependency management.
Characteristics of a Monorepo
- Consistent codebase: Since all projects live in the same repository, you can enforce consistent code style and conventions across all of them.
- Code reusability: Common modules and libraries can be easily shared and reused.
- Dependency management: Dependencies between projects are much easier to manage.
- Unified build and deployment: You can build and deploy the entire project at once, making operations more efficient.
What if You Don't Use a Monorepo?
The approach where each repository contains exactly one project is called a polyrepo. For simple projects, polyrepo can actually be easier to manage — but as projects grow and become more complex, several pain points emerge:
- Code duplication: Common modules tend to get duplicated across multiple repositories.
- Dependency conflicts: Different repositories may use different versions of the same library, leading to conflicts.
- Inconsistency: Code style and conventions can differ from repo to repo.
- Complex builds and deployments: Each repository must be built and deployed individually, making management complicated.
Code duplication can be addressed by publishing shared modules to npm or GitHub Packages, but that introduces additional overhead for versioning and release management.
Also, the larger a polyrepo project grows, the harder it becomes to migrate to a monorepo later. Personally, I prefer adopting a monorepo from the start.
Setting Up a Monorepo with pnpm Workspaces
A monorepo can be set up using the workspace feature provided by package managers like npm, yarn, or pnpm. Package managers help treat each project as an independent package and make it easy to link them together.
What is a Workspace?
A workspace allows you to manage multiple packages — each with their own package.json — within a single repository. This makes it straightforward to connect packages inside a monorepo.
Configuring pnpm Workspaces
Let me walk through setting up a monorepo from scratch using pnpm.
First, create a pnpm-workspace.yaml file to define which packages are part of the workspace:
packages:
- 'apps/*'
- 'packages/*'
- The
packagesfield specifies path patterns for packages included in the workspace. - It's common to create
appsandpackagesdirectories:-
apps/— contains applications that will actually be deployed. -
packages/— contains shared modules and libraries used across multiple apps.
-
- You're not required to use exactly
appsandpackages— feel free to structure it however makes sense for your project.
Linking Packages: Step 1 — Configure the Package
Packages within a workspace can depend on each other. For example, let's say apps/blog depends on packages/ui.
First, configure packages/ui/package.json:
//packages/ui/package.json{"name":"@repo/ui",//Packagename"private":true,//Important!Preventsaccidentalpublishingtonpm"version":"0.0.0","type":"module","main":"./src/entry.ts",//Entrypoint"types":"./src/entry.ts"//Typedefinitionfile//...}
- The
namefield sets the package name — it must be unique within the workspace. Adding a scope like@repomakes it clear that this package belongs to the monorepo. - Setting
private: trueprevents the package from being accidentally published to npm.
Important: You need to explicitly expose the files that other packages will consume.
Configure this just like any npm package — usually via main, types, or exports fields. One interesting detail in this example: we're exposing .ts source files directly. This means the consuming app handles TypeScript compilation rather than the package itself, so there's no need to build each package separately — quite convenient.
If the consuming app can't compile TypeScript from node_modules, you can configure the package to expose compiled .js files instead.
Linking Packages: Step 2 — Add the Dependency
Now let's add @repo/ui as a dependency in apps/blog. Add it to dependencies or devDependencies, then run pnpm install:
//apps/blog/package.json{"name":"blog","version":"1.0.0",//..."dependencies":{//Useworkspace:*toreferencelocalworkspacepackages"@repo/ui":"workspace:*"}}
| Key | Description |
|---|---|
@repo/ui |
Name of the monorepo package to add as a dependency |
workspace:* |
Tells the package manager to resolve this from the local workspace |
Without workspace:*, the package manager would attempt to find @repo/ui on the npm registry rather than locally.
How the Package Manager Links Packages
So how does pnpm actually connect workspace packages? In the JavaScript ecosystem, installed modules are stored in node_modules, and bundlers resolve imports from there.
Let's see how @repo/ui shows up in apps/blog/node_modules:
You'll find @repo/ui in apps/blog/node_modules, but with a ↳ icon next to it — indicating it's a symbolic link, not a physical copy of the files.
This brings two key benefits:
- Disk space savings: The package is linked rather than copied.
- Real-time change propagation: Any changes to the package source are immediately reflected without rebuilding.
Using the Package
Now you can import from @repo/ui in apps/blog:
import './style.css';
import { setupCounter } from '@repo/ui';
setupCounter(document.querySelector<HTMLButtonElement>('#counter'));
What you can import is determined by the main, types, and exports fields in packages/ui/package.json. If an import fails, check these two things:
- The package is listed in
dependenciesordevDependenciesandpnpm installhas been run. - The file you're trying to import is actually exposed in the package's
package.json.
The full example is available in the pnpm monorepo example repo.
Turborepo
Turborepo is a monorepo management tool built by Vercel (the team behind Next.js), and it's become one of the most popular tools in this space.
Here's an important detail: Turborepo builds on top of your package manager's workspace feature. So the pnpm workspace setup above stays essentially the same. So why use Turborepo?
Benefits of Turborepo
| Benefit | Description |
|---|---|
| High-performance build system | Uses change-based caching to only rebuild what changed, dramatically speeding up builds. |
| Caching & parallel execution | Caches build artifacts and runs tasks in parallel to minimize build time. |
| Unified workflow | Integrates tasks like testing, linting, and deployment into a consistent pipeline. |
Think of Turborepo as filling in the gaps that pnpm workspaces leave behind.
Adding Turborepo to Your Monorepo
Starting from the pnpm workspace project, add turbo as a dev dependency at the workspace root:
# --workspace-root installs at the monorepo root level
pnpm add turbo --save-dev --workspace-root
Defining a Build Pipeline with turbo.json
Create a turbo.json file to configure the build pipeline — this defines and orchestrates tasks like builds, tests, and lints:
{"$schema":"https://turborepo.dev/schema.json","tasks":{"build":{//Thistaskdependsonthebuildtasksofupstreampackages"dependsOn":["^build"],//Inputfiles—ifthesechange,thecacheisinvalidated"inputs":[],//Cachethesebuildoutputs"outputs":["dist/**",".next/**","!.next/cache/**"]},"lint":{},"test":{}}}
| Key | Description |
|---|---|
$schema |
Schema for the turbo config file |
tasks |
Defines tasks runnable via the turbo CLI |
dependsOn |
Declares task dependencies |
inputs |
Controls cache invalidation |
outputs |
Specifies build artifacts to cache |
With "dependsOn": ["^build"], the build task for any package will first run the build tasks of its dependencies. In our project, since apps/blog depends on packages/ui, running build for apps/blog will automatically trigger packages/ui's build first.
Turborepo manages build ordering based on your dependency graph automatically — no manual orchestration needed.
Running Turbo Commands
Run the build pipeline from the project root:
pnpm turbo run build
Turbo will execute each package's build task in the correct order according to the configured pipeline.
If that's too verbose, add a shorthand script to your root package.json:
//package.json{"scripts":{"build":"turbo run build"}}
Now you can simply run:
pnpm run build
The full Turborepo example is available in the Turborepo example repo.
Wrapping Up
We've covered the concept of monorepos and how to set one up using pnpm workspaces and Turborepo. As a project scales, a monorepo really shows its value — boosting consistency and operational efficiency across your codebase. I'd encourage you to consider the monorepo approach for your next frontend project.