Monorepository with TypeScript: Code Organization and Best Practices

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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└── .gitignore


Root 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

Blogs

Book Recommendations