monorepo/
├── tsconfig.base.json
├── apps/
│ ├── backend/tsconfig.json
│ └── frontend/tsconfig.json
└── packages/
└── shared/tsconfig.json
Only universal options — no paths or includes:
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"target": "ES2021"
}
}{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"lib": ["ES2021"],
"types": ["node"],
"outDir": "./dist",
"rootDir": "./src",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"incremental": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"types": ["vite/client"],
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules"]
}{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"lib": ["ES2021"],
"outDir": "./dist",
"rootDir": "./src",
"composite": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}Only NestJS needs these — it relies on runtime reflection for dependency injection. React and plain packages never need them. If a shared package exports NestJS decorators or injectable classes, that package also needs them.
| Context | Value | Reason |
|---|---|---|
| Backend | ["ES2021"] |
Node only, no browser APIs |
| Frontend | ["ES2021", "DOM", "DOM.Iterable"] |
Needs window, document, HTMLElement, etc. |
| Shared package | ["ES2021"] |
Use the narrowest lib that works |
If a shared package has code touching the DOM, it needs "DOM" too — but that's a smell, it probably belongs in the frontend.
| Context | module |
moduleResolution |
Reason |
|---|---|---|---|
| NestJS | commonjs |
node |
NestJS compiler and Node expect require() |
| React + Vite | ESNext |
bundler |
Vite handles module resolution |
| Shared package | commonjs |
node |
Match your primary consumer (usually NestJS) |
Only the frontend and any shared UI component package need "react-jsx". Everything else omits it.
- React app:
true— Vite does the actual compilation; TypeScript is only type-checking. - NestJS + packages:
false(default) —tscactually emits the JavaScript.
Shared packages set this to true to enable TypeScript project references, which lets consuming apps do incremental builds. Apps generally don't need it.
| Context | Value | Reason |
|---|---|---|
| Backend | ["node"] |
For process, Buffer, __dirname, etc. |
| Frontend | ["vite/client"] |
For Vite-specific globals like import.meta.env |
This keeps type pollution out — your backend won't accidentally use document and your frontend won't accidentally use process.env without import.meta.env.
The cleanest approach in a pnpm monorepo is to point the package's main/types in its package.json directly at the source:
{
"main": "./src/index.ts",
"types": "./src/index.ts"
}Let each consuming app's build tool compile it. That way the package tsconfig is just for IDE support and type-checking, and you avoid the CJS-vs-ESM headache entirely.