Monorepo with TypeScript: Code Organization in Practice

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

Monorepo with TypeScript: Code Organization in Practice

\n\n

In 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\n

What is a monorepo and why do you need it?

\n\n

A 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
\n\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\n

Monorepo Structure: Basic Setup

\n\n

Let'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).

\n\n

1. Root Configuration

\n\n

Let's start with the project root. Create a package.json file with workspaces:

\n\n
{\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\n

Then configure the root tsconfig.json. We use Project References — this is a key TypeScript feature for monorepos:

\n\n
{\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\n

Important: the composite: true option is mandatory for packages that are referenced by other packages. It enables incremental builds and generation of .d.ts files.

\n\n

2. Configuring the core Package

\n\n

Go to packages/core. Create a package.json:

\n\n
{\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\n

And a tsconfig.json for this package:

\n\n
{\n  "extends": "../../tsconfig.json",\n  "compilerOptions": {\n    "outDir": "./dist",\n    "rootDir": "./src"\n  },\n  "include": ["src"]\n}\n
\n\n

Create a simple module src/index.ts:

\n\n
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\n

Now the package

Blogs

Book Recommendations