logohsb.horse
← Back to blog index

Blog

TypeScript Monorepo Best Practices for 2026

A practical TypeScript monorepo guide for 2026. Compare pnpm + Turborepo vs pnpm + Biome, when to use Project References, and when each setup makes sense.

Published:Updated:

Translations

If you are setting up a TypeScript monorepo in 2026, start with pnpm workspace + TypeScript Project References. Add Turborepo when build time, task orchestration, CI caching, or Docker packaging becomes a real bottleneck. If your repo is still a simple split like apps/web, apps/wxt, packages/ui, and packages/core, pnpm workspace + Biome + TypeScript Project References is usually enough.

This article compares those two setups, explains when pnpm + Turborepo is worth the added complexity, and shows how to keep package boundaries clean. The goal is not to collect every tool up front, but to add complexity only when the repo has earned it.

TL;DR

  • Start with pnpm workspace + TypeScript Project References.
  • Add Turborepo when build, test, lint, or CI time becomes painful.
  • If you have a small apps/web + apps/wxt + shared packages repo, pnpm workspace + Biome is often enough.
  • Use workspace: and exports for cross-package boundaries. Do not rely on tsconfig.paths alone.
Prerequisites First configuration to choose Supplement
New generic monorepo. In the future, package release, CI optimization, and Docker integration will be considered pnpm workspace + Turborepo + TypeScript Project References + package.json of exports + Changesets The most stable
4 package configuration: apps/web, apps/wxt, packages/ui, packages/core. I don’t use Docker. Do not publish shared packages to npm pnpm workspace + Biome + TypeScript Project References Turborepo is not needed at this stage
Bun provides the execution environment, package manager, and testing bun workspace is also a candidate However, pnpm is strong in terms of safe operation

In short, pnpm + turbo is the strongest general-purpose answer. However, in this specific four-package setup, workspace only is often enough.

Default setup for a new TypeScript monorepo

As of 2026, pnpm workspace is still the safest default foundation for a TypeScript monorepo. Workspaces are built in, local package references are explicit with the workspace: protocol, and --filter gives you precise control over which packages to run against.

Use TypeScript Project References for type boundaries. The TypeScript official documentation also clearly states that split projects can improve build time, logical separation, and build in dependency order using tsc --build. This is quite effective in monorepo.

If you add Turborepo on top of that, you get task graphs, caching, parallel execution, and turbo prune in one step. Turborepo task configuration is mostly about declaring dependsOn and outputs, so it scales well as the repo grows.

If you have a package to publish, Changesets will naturally fit in as well. pnpm’s workspace documentation also assumes that a dedicated tool such as Changesets is used for versioning of packages in a workspace.

For a general-purpose repo, the default stack looks like this.

Role Recommendation
workspace management pnpm workspace
Type Boundaries and Incremental Builds TypeScript Project References
Task execution, parallelization, and caching Turborepo
public face of package boundary exports of package.json
versioning / changelog Changesets
lint / format Biome or ESLint + Prettier. It’s easier to start with Biome now

When Turborepo is still unnecessary

The configuration this time is fairly simple.

packages/core
packages/ui -> packages/core
apps/web -> packages/ui, packages/core
apps/wxt -> packages/ui, packages/core

If the dependencies are at this level, running the filter on pnpm workspace and tsc -b is sufficient for operation.

The value of Turborepo jumps quickly in situations like these.

Pain Why Turborepo works
Total build / test / lint time is long Caching and parallel execution are effective
I want to clarify the dependency order of tasks dependsOn can express a graph
I want to speed up CI Remote cache works
I want to extract only the necessary packages for Docker turbo prune can be used

On the other hand, if you install it at a stage where you are not yet in such trouble, it is difficult to see the value beyond adding one more configuration file. If you turn off Docker, the appeal of prune will drop a notch. Changesets is also not required if you do not publish shared packages on npm.

Therefore, based on this premise, it is reasonable to start with pnpm workspace + Biome + TypeScript Project References.

How to keep package boundaries clean

In this four-part division, it is more important to avoid ambiguity in roles than in tool selection.

Path Things to put Things you shouldn’t put
packages/core domain, schema, validation, API client, pure function, state model UI, routing, browser API
packages/ui Environment-independent React component / hook WXT-specific processing, router, extension storage
apps/web Assembly as a web application, page, routing, adapter extension specific circumstances
apps/wxt background / content script, browser API, WXT-specific settings Web-specific routing, pure logic that can be placed in the shared layer

The last thing I want to avoid is turning packages/ui into a place for all things that are shared. When browser extension specific circumstances and routing start to enter here, it becomes a connection point rather than a shared layer. If that happens, the benefits of monorepo will diminish.

Notes for apps/wxt

You should be a little careful about apps/wxt. WXT TypeScript settings usually extend .wxt/tsconfig.json from the root tsconfig.json. In a monorepo you may not take that shape, in which case you need to include .wxt/wxt.d.ts in the TypeScript project for apps/wxt.

In other words, although apps/wxt is part of the monorepo, it is more stable to treat it as an independent TypeScript project.

If your extension also uses React, How to Enable React Compiler in WXT is a good example of keeping WXT-specific build settings inside the app package instead of leaking them into shared packages.

Do not use tsconfig.paths as your main cross-package boundary

This is quite important.

TypeScript 5.7 release notes clearly states that imports that depend on baseUrl and paths cannot be rewritten when outputting JS. In other words, a design that crosses package boundaries using only tsconfig.paths may pass type checking, but is likely to be distorted at the execution or publishing stage.

Basically, you should consider cross-border references in monorepo in the following order.

  1. Separate packages
  2. Connect dependencies with workspace:
  3. Decide the public side with exports of package.json
  4. Create a dependency order build with TypeScript Project References

paths is less likely to be damaged if it is kept as an auxiliary within the package.

Why Biome fits this setup

I think the decision to remove ESLint and focus on Biome is quite natural. Biome’s big projects guide also organizes the premise of using root config and nested config differently for large repos such as monorepo and workspace. In v2, a configuration based on monorepo is also introduced.

At this scale, it is usually more straightforward to standardize lint and format on Biome and keep project boundaries and builds on the TypeScript side.

If you are standardizing Biome across the repo, these follow-ups are useful:

Minimal setup checklist

This is enough for the initial setup.

  1. Place pnpm-workspace.yaml in root
  2. Place solution-style tsconfig.json in root and references each package
  3. packages/core and packages/ui enable composite
  4. Shared package specifies exports of package.json
  5. Place biome.json in root
  6. For apps/wxt, check separately how to handle WXT type definitions.

At this point, you can fight for quite a long time.

When to add the next tool

What to add When to add
Turborepo Build / test / lint is heavy, CI optimization is required, I want to clarify the task graph
Changesets I want to version-manage and publish a shared package
Nx I wanted automatic generation, polyglot, and stronger monorepo management
Migrating to bun workspace I wanted to unify runtime / package manager / test to Bun

With monorepo, it works better to add only where the pain occurs, rather than adding everything from the beginning.

Summary

There is no single optimal solution for TypeScript monorepo. pnpm workspace + Turborepo + TypeScript Project References is strong for general-purpose non-difficult answers. However, if Docker and package publishing are not too heavy yet after dividing into apps/web, apps/wxt, packages/ui, and packages/core, it would be lighter and less likely to break down if you start with pnpm workspace + Biome + TypeScript Project References.

The important thing is not to increase the number of tools, but to keep core and ui thin, and express cross-border references honestly with workspace: and exports. If that doesn’t collapse, it’s not too late to add turbo later.

Reference

Related posts