logo hsb.horse
← Back to blog index

Blog

TypeScript Monorepo Organizing the optimal solution 2026 edition

We organized the roles of pnpm workspace, Turborepo, TypeScript Project References, Biome, and WXT, and summarized the general-purpose simple solution and the practical solution with a 4-package structure.

Published:

When you start thinking about TypeScript monorepos, your options quickly expand. pnpm, bun, Turborepo, Nx, Changesets, Biome. All of them seem correct, but if you put them all in from the beginning, it will be heavy. On the other hand, if you cut everything, you will be in trouble later.

The reason this kind of discussion is difficult to organize is that we only look for the “optimal solution” without considering the premises. It is better to separate the general-purpose, easy-to-understand solution and the practical solution for the configuration in front of you.

This time, I will organize the following two parts.

  1. Safe configuration when creating a new TypeScript monorepo
  2. Realistic configuration based on the specific assumptions of apps/web, apps/wxt, packages/ui, packages/core

First, the conclusion

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 strong in general theory. However, in this case, workspace only is sufficient.

General purpose non-difficult

As of 2026, if you want to start without worrying deeply, pnpm workspace is the most stable foundation. workspace is provided as standard, local package references can be made explicit with workspace: protocol, and target packages can be narrowed down considerably with --filter.

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 will have task graph, cache, parallel execution, and turbo prune all at once. [Turborepo’s task settings] (https://turborepo.dev/docs/crafting-your-repository/configuring-tasks) only needs to be assembled around dependsOn and outputs, and there is room for growth 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.

In other words, the general-purpose non-difficult solution is as follows.

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

Why is Turborepo still not needed in this premise?

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.

For example, the value of including Turborepo increases all of a sudden in situations like this.

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 cut layers

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 on apps/wxt

You should be a little careful about apps/wxt. WXT TypeScript settings usually extends .wxt/tsconfig.json from root’s tsconfig.json. However, monorepo may not take that form, in which case you need to include .wxt/wxt.d.ts in your TypeScript project.

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

Do not use tsconfig.paths as the primary means of cross-border references

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.

Biome fits well with this premise

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 would be more straightforward to send the lint/format to Biome and take care of only the project boundary and build on the TypeScript side.

Minimal configuration image

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