logo hsb.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:

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.
PrerequisitesFirst configuration to chooseSupplement
New generic monorepo. In the future, package release, CI optimization, and Docker integration will be consideredpnpm workspace + Turborepo + TypeScript Project References + package.json of exports + ChangesetsThe most stable
4 package configuration: apps/web, apps/wxt, packages/ui, packages/core. I don’t use Docker. Do not publish shared packages to npmpnpm workspace + Biome + TypeScript Project ReferencesTurborepo is not needed at this stage
Bun provides the execution environment, package manager, and testingbun workspace is also a candidateHowever, 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.

RoleRecommendation
workspace managementpnpm workspace
Type Boundaries and Incremental BuildsTypeScript Project References
Task execution, parallelization, and cachingTurborepo
public face of package boundaryexports of package.json
versioning / changelogChangesets
lint / formatBiome 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.

PainWhy Turborepo works
Total build / test / lint time is longCaching and parallel execution are effective
I want to clarify the dependency order of tasksdependsOn can express a graph
I want to speed up CIRemote cache works
I want to extract only the necessary packages for Dockerturbo 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.

PathThings to putThings you shouldn’t put
packages/coredomain, schema, validation, API client, pure function, state modelUI, routing, browser API
packages/uiEnvironment-independent React component / hookWXT-specific processing, router, extension storage
apps/webAssembly as a web application, page, routing, adapterextension specific circumstances
apps/wxtbackground / content script, browser API, WXT-specific settingsWeb-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 addWhen to add
TurborepoBuild / test / lint is heavy, CI optimization is required, I want to clarify the task graph
ChangesetsI want to version-manage and publish a shared package
NxI wanted automatic generation, polyglot, and stronger monorepo management
Migrating to bun workspaceI 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