Monorepo with TypeScript: Code Organization in Practice
\n\nIn modern web development, especially when working with TypeScript, developers often face a dilemma: store all code in one repository (monorepo) or split it into many small repositories (polyrepo). For medium and large projects where code reusability and unified configuration are important, the monorepo becomes the de facto standard. In this article, we will explore how to properly organize a monorepo in TypeScript, from the basic structure to advanced techniques.
\n\nWhat is a monorepo and why do you need it?
\n\nA monorepo is an approach where the code of several projects (packages, libraries, applications) is stored in a single repository. In the context of TypeScript, this is especially convenient as it allows you to:
\n\n- \n
- Reuse types and interfaces between different parts of the system without publishing to npm. \n
- Simplify refactoring: a change in one package is automatically validated in all dependents. \n
- Have a unified configuration for the linter, formatter, and bundler. \n
- Simplify CI/CD: one repository — one pipeline. \n
Among the popular tools for working with monorepos in TypeScript, Nx, Turborepo, and npm/yarn/pnpm workspaces stand out. We will look at an approach using pnpm workspaces and TypeScript Project References, as this is a minimal and flexible set.
\n\nMonorepo Structure: Basic Setup
\n\nLet's create a typical monorepo structure for a TypeScript project. Suppose we have three packages: core (common types and utilities), server (backend), and web (frontend).
1. Root Configuration
\n\nLet's start with the project root. Create a package.json file with workspaces:
{\n "name": "my-monorepo",\n "private": true,\n "workspaces": [\n "packages/*"\n ],\n "scripts": {\n "build": "tsc -b",\n "clean": "tsc -b --clean",\n "lint": "eslint packages/*/src"\n }\n}\n\n\nThen configure the root tsconfig.json. We use Project References — this is a key TypeScript feature for monorepos:
{\n "compilerOptions": {\n "target": "ES2020",\n "module": "commonjs",\n "strict": true,\n "composite": true,\n "declaration": true,\n "declarationMap": true,\n "sourceMap": true,\n "outDir": "./dist",\n "rootDir": ".",\n "skipLibCheck": true\n },\n "references": [\n { "path": "./packages/core" },\n { "path": "./packages/server" },\n { "path": "./packages/web" }\n ]\n}\n\n\nImportant: the composite: true option is mandatory for packages that are referenced by other packages. It enables incremental builds and generation of .d.ts files.
2. Configuring the core Package
\n\nGo to packages/core. Create a package.json:
{\n "name": "@my/core",\n "version": "1.0.0",\n "main": "dist/index.js",\n "types": "dist/index.d.ts",\n "scripts": {\n "build": "tsc -b"\n }\n}\n\n\nAnd a tsconfig.json for this package:
{\n "extends": "../../tsconfig.json",\n "compilerOptions": {\n "outDir": "./dist",\n "rootDir": "./src"\n },\n "include": ["src"]\n}\n\n\nCreate a simple module src/index.ts:
export interface User {\n id: string;\n name: string;\n email: string;\n}\n\nexport function greet(user: User): string {\n return `Hello, ${user.name}!`;\n}\n\n\nNow the package
Blogs
Book Recommendations
Read also
Singleton in C++: How Does It Work Under the Hood? Breakdown of a Legendary Question
Design Patterns in Go with Examples for Developers
How a Rust program reads its metadata from Cargo.toml
Java Projects for Portfolio: From Beginner to Junior in 2025
Top Mistakes of Beginner Swift Developers: From A to Z