Architecting High-Traffic Apps Around TypeScript Project References

When a critical production hotfix stalled in our deployment pipeline due to an out-of-memory compiler crash, rebuilding our monorepo around TypeScript Project References became our most important reliability upgrade.

VW
Viery Wahyu•Author

Nobody cares about compiler configuration files when traffic is calm and deployments take their time in the background. You only start caring about how TypeScript resolves your module graph when a flash sale spikes your concurrent checkout requests by four hundred percent, a subtle race condition starts locking up database connections, and your two-line emergency hotfix cannot reach production because the continuous integration runner keeps dying with an out-of-memory error. That exact nightmare happened to us during a peak operational window last year. While our engineers watched error rates climb on our live telemetry dashboards, our deployment pipeline spent fourteen agonizing minutes choking on a monolithic type-checking step before crashing completely.

How Naive Path Aliases Slowly Poisoned Our Monorepo

When we conducted our post-incident retrospective the next morning, we discovered that the root cause of the deployment outage had been hiding inside our root TypeScript configuration for months. Like many fast-moving engineering teams using a pnpm workspace, we had originally set up our shared database schemas, domain engines, and frontend applications using simple TypeScript path aliases. Whenever our Next.js storefront or our Node.js worker needed a shared utility or a Drizzle table definition, it simply reached across folder boundaries using a wildcard path mapping.

json
1{
2 "compilerOptions": {
3 "target": "ES2022",
4 "module": "ESNext",
5 "moduleResolution": "bundler",
6 "strict": true,
7 "baseUrl": ".",
8 "paths": {
9 "@repo/db/*": ["packages/db/src/*"],
10 "@repo/core-engine/*": ["packages/core-engine/src/*"],
11 "@repo/ui/*": ["packages/ui/src/*"]
12 }
13 }
14}

Pointing path aliases directly at raw source directories feels frictionless during early development, but it quietly destroys compiler scalability as your codebase grows. Because TypeScript did not treat those shared folders as pre-compiled, bounded packages, running tsc --noEmit inside our main web application forced the compiler to parse, bind, and check the entire abstract syntax tree of every single backend service, database migration utility, and shared UI component from scratch in a single massive memory space. Worse still, because there were no strict boundary walls enforced by the compiler, a junior developer had accidentally imported a server-side validation helper that transitively pulled heavyweight database cryptography libraries straight into a client-side React component, bloating both our memory footprint during compilation and our browser JavaScript bundles under high traffic.

Carving the Monolith Into an Incremental Dependency Graph

Preventing another blocked hotfix meant treating our internal workspace packages with the exact same architectural rigor as external third-party libraries. We ripped out the global source-level path aliases and restructured the entire monorepo around TypeScript Project References. Instead of letting the compiler swallow hundred-thousand-line source trees whole, Project References divide the codebase into isolated compilation units connected through an explicit directed acyclic graph.

Every shared package in our workspace received a dedicated configuration with the composite flag enabled alongside declaration and declarationMap generation. This tells TypeScript to emit lightweight .d.ts contract files and an incremental .tsbuildinfo state cache whenever a package is built, allowing downstream applications to check types against pre-computed interfaces rather than re-parsing raw implementation code.

json
1{
2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "composite": true,
5 "declaration": true,
6 "declarationMap": true,
7 "outDir": "./dist",
8 "rootDir": "./src"
9 },
10 "include": ["src/**/*"],
11 "references": [
12 { "path": "../shared-types" }
13 ]
14}

Once the foundational packages were configured as composite units, we wired our high-traffic customer application to explicitly declare which internal boundaries it was legally allowed to cross. By separating pure data contracts in @repo/shared-types from server-only database drivers in @repo/db, we could grant our frontend client code access to shared types while making it physically impossible for client components to reference backend infrastructure modules.

json
1{
2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "composite": true,
5 "noEmit": false,
6 "incremental": true,
7 "tsBuildInfoFile": "./.tsbuildinfo"
8 },
9 "include": ["src/**/*", "next-env.d.ts"],
10 "references": [
11 { "path": "../../packages/shared-types" },
12 { "path": "../../packages/ui" }
13 ]
14}

Enforcing Zero-Leakage Boundaries and Instant CI Hotfixes

Transitioning from standard type checking to orchestrating tsc --build across the workspace transformed both our runtime performance and our incident response speed. When TypeScript runs in build mode, it inspects the project reference graph, verifies the cryptographic signatures inside each package's .tsbuildinfo file, and completely skips any upstream package whose public interface has not changed.

sh
1# Caching and verifying the entire monorepo dependency graph in seconds
2pnpm tsc --build tsconfig.build.json --verbose

When we simulated our previous production incident after rolling out Project References, modifying a two-line concurrency lock inside a backend worker took less than four seconds to type-check in our continuous integration pipeline. Because the change did not alter the exported .d.ts contract of the core engine, the compiler verified the single modified file and skipped re-evaluating the storefront and admin portals entirely. Peak memory consumption on our build runners dropped from over eight gigabytes to less than nine hundred megabytes, eliminating out-of-memory crashes for good.

Why Compiler Boundaries Are Production Infrastructure

Look closely at any high-traffic web platform that survives scale, and you will find that architectural boundaries are enforced by tooling rather than human discipline alone. Relying on code reviews to stop server-side dependencies from leaking into client bundles or hoping that a single monolithic type-check will scale forever is a recipe for a prolonged production outage when every second counts. Architecting our monorepo around TypeScript Project References turned our compiler from a fragile deployment bottleneck into a deterministic guardrail, ensuring that when production traffic surges and an urgent fix needs to ship, our pipeline moves just as fast as our engineers.

VW

Written by

Viery Wahyu

📍 Bali, Indonesia

Software Engineer

Software engineer based in Bali with over a decade of experience building fast web applications

The Journal

View all →