Introduction to Monorepos with TypeScript
With the growing complexity of modern web applications, developers increasingly face the problem of code management. When a project consists of many interconnected modules — frontend, backend, shared libraries — the traditional approach with separate repositories (polyrepo) can lead to code duplication, version desynchronization, and difficulties in refactoring.
A monorepo is a strategy where the code of several projects is stored in a single repository. Combined with TypeScript, a monorepo becomes a powerful tool: strict typing ensures the safety of changes across the entire stack, and a unified build system simplifies dependency management.
In this article, we will look at how to organize a monorepo with TypeScript: from the basic structure to advanced tools like Nx and Turborepo.
Benefits of a Monorepo for TypeScript Projects
Before moving to practice, let's understand why a monorepo is especially beneficial for TypeScript projects:
- Unified Typing — TypeScript allows reusing interfaces and types between packages without duplication.
- Atomic Commits — changes to an API and its consumers can be made in a single commit, simplifying review and rollback.
- Simplified Refactoring — IDEs (e.g., VS Code) correctly handle renames and file moves across the entire repository.
- Build Caching — tools like Nx cache compilation results, speeding up CI/CD.
Basic Monorepo Structure
Let's start with a simple but effective structure. We'll create a root project and several packages:
my-monorepo/├── packages/│ ├── shared/ # Shared types and utilities│ │ ├── src/│ │ ├── package.json│ │ └── tsconfig.json│ ├── server/ # Backend (e.g., Express)│ │ ├── src/│ │ ├── package.json│ │ └── tsconfig.json│ └── web/ # Frontend (React)│ ├── src/│ ├── package.json│ └── tsconfig.json├── package.json # Root package.json├── tsconfig.base.json # Base tsconfig for all packages└── .gitignoreRoot package.json should contain the workspace configuration. For npm and Yarn, it looks like this:
{ "name": "my-monorepo", "private": true, "workspaces": [ "packages/*" ], "scripts": { "build": "tsc --build", "clean": "tsc --build --clean" }}Base tsconfig.base.json sets common compilation settings:
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, "declarationMap": true, "sourceMap": true, "composite": true }}Each package extends the base config:
{ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src"]}Managing Dependencies and TypeScript paths
One of the key challenges of monorepos is correctly configuring imports between packages. Instead of publishing packages to npm, we use TypeScript Project References and paths.
Setting up Project References
Let's add references to the projects in the root tsconfig.json:
{ "files": [], "references": [ { "path": "./packages/shared" }, { "path": "./packages/server" }, { "path": "./packages/web" } ]}Now when running