# ts-monorepo: a template where Go to definition works on a fresh clone

> ts-monorepo is a TypeScript monorepo template whose stated goal is that Go to definition works in an IDE immediately after a fresh clone, with a matching example for every major tool in the ecosystem. It suits a team standing up a shared library and application repository, and the path alias approach is the choice everything else is built around.

**NiGhTTraX/ts-monorepo** — Template for setting up a TypeScript monorepo

- Repository: https://github.com/NiGhTTraX/ts-monorepo
- Stars: 1,630 · Forks: 178
- Language: TypeScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/nighttrax-ts-monorepo

## Two goals, and the first one is unusually concrete

ts-monorepo is a template project for setting up a TypeScript monorepo, and the documentation states two goals.

The main focus is making the Go to definition feature in IDEs work without any surprises, meaning it will work after a fresh clone without needing to build the project. That is a sharper goal than most templates state, and it is the one that decides the design.

The reason it is hard is that a monorepo with path aliases has two separate consumers of the same mapping. The TypeScript compiler resolves aliases itself, through the `paths` and `baseUrl` options in `tsconfig.json`. Every other tool in the chain, the bundler, the test runner, the server framework, the storybook, does not, and each has its own mechanism or its own plugin for it. A monorepo that works in the editor and fails in the build, or works in the build and fails in the tests, is the standard failure, and it is entirely a path alias problem.

So this template treats the alias as the central abstraction, and everything else is a demonstration of how one particular tool learns about it.

The secondary focus is to remove surprises when publishing packages. The repository is set up so that each package gets a clean build output without any artifacts from other packages.

That second goal is the one that catches teams out, and the reason is the structure described in the packages and apps section. A package is meant to be published to npm and installed, and for packages you do not want to bundle all the monorepo dependencies, and instead publish them individually. That is why packages have a separate build `tsconfig.json` that resolves monorepo dependencies to `node_modules`.

If you do not separate the two builds, a published package ships with a bundle that has your sibling library inlined, and your consumers end up with two copies of the same code.

## Packages and apps, and why the separation is structural

The repository contains two types of workspaces, and the documentation is explicit about the difference.

`packages` are meant to be published to npm and installed. `apps` are meant to be executed. The example given is create-react-app, since you would not publish an app like that to npm, you would run it, and more specifically you would build the JS bundle and then deploy that somewhere.

That distinction drives the build configuration. For packages, you do not want to bundle all the monorepo dependencies, and instead publish them individually, which is why packages have a separate build `tsconfig.json` that resolves monorepo dependencies to `node_modules`.

The mechanism is worth understanding because it is the part that is easy to get wrong. The application build can use path aliases freely, since it is bundled and the alias is resolved at build time into a relative or bundled path. The package build cannot, because a published package has to reference its dependencies by name so that the consumer's package manager resolves them. So the package build compiles the alias into a `node_modules` reference, and the result is a package that installs and works like any other.

At the root of the repository there is a `tsconfig.json` and a `tsconfig.build.json` alongside the per-package ones, and the `packages/` and `apps/` directories sit at the top level next to `pnpm-workspace.yaml` and `pnpm-lock.yaml`.

The build orchestration is done with pnpm's recursive run, so the root scripts are thin. `build` is `pnpm run -r build`, `test` is `pnpm run -r test`, `clean` is `pnpm run -r clean`, and `lint` is `eslint .` at the root. The `docs` script runs doctoc against the README, which is why the table of contents in the documentation carries the instruction not to edit it by hand but to re-run doctoc instead.

## pnpm, and the hoisting problem behind the recommendation

The repository uses pnpm, and the documentation says it should work fine with yarn workspaces, with npm 7 or later workspaces, or with npm below 7 and `lerna bootstrap`. Then it makes a strong recommendation: pnpm, not only because it is usually faster, but because it avoids dependency problems caused by hoisting, with a link to a specific commit as an example.

That is the substantive technical claim in the setup section, so it is worth being precise about what hoisting does and why it causes trouble in a monorepo.

A hoisting package manager installs a dependency once, at the top of the tree, and leaves a symlink in the workspace that needs it. That is efficient and it is the source of a class of bug that is specific to monorepos. If two workspaces need different versions of the same package, hoisting has to pick one to put at the top, and the other workspace gets a nested copy, or the resolution silently changes shape depending on what else is in the tree. The result is a build that works in isolation and breaks when a package is added somewhere else.

pnpm's default is the opposite, a strict, isolated node_modules where each workspace gets exactly the versions its own package.json declares, with the store hard linked rather than copied. The consequence for this template is that the dependency graph of a package is a function of that package's manifest rather than of the whole repository, which is what makes the build output reproducible and what makes the published package correct.

The setup itself is short:

```sh
# Install pnpm with your preferred method: https://pnpm.io/installation.
npm i -g pnpm

# Install all dependencies.
pnpm i
```

Then the instruction that matters is to customize the template to your own needs, removing the examples you do not need and adding the tools you do need, with compilation targets, module systems and tree shaking left up to you to decide.

That last point is the template's philosophy. It is explicit about the structure and it deliberately does not make the build decisions for you.

## One example per tool, and the alias configuration each needs

The integrations section is the heart of the template, and it is organised as one subsection per tool, each pointing at a full example in the `apps/` directory.

For ts-node, the alias is resolved at runtime using tsconfig-paths, with the start script set to `ts-node -r tsconfig-paths/register src/index.ts`.

For Babel, the alias is resolved by babel-plugin-module-resolver, with a preset for the current node target, the TypeScript preset, and a plugin configured with an alias mapping `^@nighttrax/(.+)` to the package's `src` directory.

For webpack, the tool is tsconfig-paths-webpack-plugin, added to the resolve plugins array.

For jest there are two routes. If you use Babel, the Babel example applies. If you use ts-jest, the `pathsToModuleNameMapper` helper converts the compiler options into a module name mapper, with a comment noting that the prefix has to match the baseUrl defined in tsconfig.json. That comment is the whole trap in one line, since the mapper paths are relative to the Jest rootDir and the tsconfig baseUrl is not.

For Vite, the plugin is vite-tsconfig-paths, added alongside the React plugin in the defineConfig plugins array.

For NextJS, the configuration extends Next's webpack config, finds the TypeScript rule, clears its include and sets its exclude to node_modules so that Babel compiles outside of `src/`, which is what lets a package from another workspace be compiled rather than treated as an external dependency.

For NestJS, the path aliases go in both `tsconfig.json` and `tsconfig.build.json`, and the schematics collection points the entry file at the app's main.

For Storybook, the webpack config is extended through `webpackFinal`, and the tsconfig paths plugin is added with the extensions from the existing config.

So the pattern is consistent. Every tool needs to be told about the alias, the mechanism differs, and the template shows the working configuration for each rather than a general principle.

There is one alternative worth knowing about, and the documentation points to it directly. If you are looking for the project references solution, there is a `project-references` branch in the repository, which is the other way to solve the same problem in a TypeScript monorepo.

## Where this template stops being the right starting point

The design is coherent and the trade-offs are known, so it is worth being clear about the boundaries.

The first is the size of the example set. The template carries a working example for ts-node, Babel, webpack, jest with both Babel and ts-jest, Vite, NextJS, NestJS, Storybook and create-react-app. That is a lot of code to delete before you have a project, and the documentation's own instruction is to remove the examples you do not need. But the removal is a task, and a team that does not do it ends up maintaining a template with a stack of half-configured app directories.

The second is that the alias approach is not the only way to build a monorepo. Path aliases are a resolution trick, and the alternative in TypeScript is project references, which is what the `project-references` branch contains. The trade is real: aliases make the source tree look like one project, which is friendly to editors and to the build, while project references make the compilation graph explicit, which is what you want when the build order and the incremental compilation matter. The template chooses aliases and the branch chooses references, and a team that outgrows aliases has to redo the build configuration.

The third is the dependency on the pnpm recommendation being accepted. The template works with yarn workspaces and npm 7 workspaces, but the author recommends pnpm specifically because of the hoisting problem, and the alias configuration examples are written for the pnpm layout. A team that standardises on yarn workspaces is taking the hoisting risk knowingly.

The fourth is the tests workflow itself. The badge in the documentation is for a tests workflow, and the root test script is a recursive run, so the CI story is whatever you make of it. The template gives you the structure and the configuration, not the pipeline, and a team that needs a specific CI setup will write it.

On the practical side, the manifest pins the tooling tightly. TypeScript is at `~5.9.0`, eslint at `~10.11.0` with the `typescript-eslint` plugin at `~8.71.0`, prettier at `~3.9.0`, and the package manager is pnpm at 12.8.2. There is a `renovate.json` at the top level, so the dependency updates are automated, which is the right arrangement for a template that will be copied rather than consumed.

The licence is MIT and the last push was on 2026-10-18, so the template is current, and the documentation points to two blog posts that explain the reasoning behind the setup in more depth than the README does.

## Conclusion

ts-monorepo fits a team that has decided on a monorepo and wants the path alias plumbing solved once, because the template ships a working example per tool rather than a set of instructions, and because the packages and apps separation means a published package does not bundle its siblings. Skip it if you want a minimal starting point, since the template deliberately carries an example app for nearly every framework, and skip it if your build system is not one of the covered ones, since the alias resolution is per tool and an uncovered tool will not resolve. Before you build on it, read the two blog posts the documentation links to, and note that the repository is MIT licensed with a last push on 2026-10-18, while the pnpm version is pinned at 12.8.2.

## FAQ

### What is the ts-monorepo template for?

Its main focus is making the Go to definition feature in IDEs work without surprises, so it works after a fresh clone without building the project, and its secondary focus is giving each package a clean build output without artifacts from other packages when publishing.

### How do I set up the ts-monorepo template?

Click the Use this template button on the GitHub page, then install pnpm with `npm i -g pnpm` and run `pnpm i` to install all dependencies. Then remove the example apps you do not need.

### What is the difference between packages and apps in ts-monorepo?

Packages are meant to be published to npm and installed, apps are meant to be executed. Packages get a separate build tsconfig.json that resolves monorepo dependencies to node_modules, so they publish their dependencies individually rather than bundling them.

### How do I resolve path aliases in jest with ts-jest?

Use the `pathsToModuleNameMapper` helper from ts-jest with the compilerOptions paths, setting a prefix that has to match the baseUrl defined in tsconfig.json. The template also has a Babel based jest example.

### Does the ts-monorepo template support other package managers?

It says it should work fine with yarn workspaces, with npm 7 or later workspaces, or with npm below 7 and lerna bootstrap, but it strongly recommends pnpm because it avoids dependency problems caused by hoisting.

## Sources

- [Issues](https://github.com/NiGhTTraX/ts-monorepo/issues)
- [License: MIT](https://github.com/NiGhTTraX/ts-monorepo/blob/master/LICENSE)
- [NiGhTTraX/ts-monorepo on GitHub](https://github.com/NiGhTTraX/ts-monorepo)
- [README](https://github.com/NiGhTTraX/ts-monorepo/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/nighttrax-ts-monorepo
