Skip to content

Instantly share code, notes, and snippets.

@TayfunTurgut
Last active July 1, 2026 02:33
Show Gist options
  • Select an option

  • Save TayfunTurgut/4559b4e1f2faa376e2f2124e9f85e2ec to your computer and use it in GitHub Desktop.

Select an option

Save TayfunTurgut/4559b4e1f2faa376e2f2124e9f85e2ec to your computer and use it in GitHub Desktop.
TypeScript Config for a NestJS + React pnpm Monorepo

TypeScript Config for a NestJS + React pnpm Monorepo

Folder Layout

monorepo/
├── tsconfig.base.json
├── apps/
│   ├── backend/tsconfig.json
│   └── frontend/tsconfig.json
└── packages/
    └── shared/tsconfig.json

Root tsconfig.base.json

Only universal options — no paths or includes:

{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "sourceMap": true,
    "target": "ES2021"
  }
}

NestJS Backend — apps/backend/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "lib": ["ES2021"],
    "types": ["node"],
    "outDir": "./dist",
    "rootDir": "./src",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "incremental": true
  },
  "references": [{ "path": "../../packages/shared" }],
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

React Frontend — apps/frontend/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2021", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "types": ["vite/client"],
    "noEmit": true
  },
  "references": [{ "path": "../../packages/shared" }],
  "include": ["src"],
  "exclude": ["node_modules"]
}

Shared Package — packages/shared/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "lib": ["ES2021"],
    "outDir": "./dist",
    "rootDir": "./src",
    "composite": true,
    "declaration": true,
    "declarationMap": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Note: Pin the Module Format in package.json, Not tsconfig

With nodenext, TypeScript reads the module format from the nearest package.json rather than a fixed tsconfig value. For the backend and shared package, simply omit "type": "module" from their package.json and they default to CommonJS.

Note: Shared Packages Consumed by Both Frontend and Backend

Shared package's package.json:

{
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts"
}

Consuming apps declare a references array in their tsconfig.json. Running tsc --build then compiles shared first, followed by the apps.

Because shared now resolves to dist/ instead of source, both the frontend (Vite) and the backend dev process need shared/dist to exist. So your dev flow must build shared first — ideally in watch mode. For example, run tsc -b packages/shared --watch alongside the app, or use a pnpm/turbo script that builds shared before vite dev.


Follow-up: Plain Node / Express Server (no NestJS)

Same idea as the backend config above, minus the NestJS-specific bits. As of Node 24 LTS, Node strips TypeScript types and runs .ts files natively, so the dev loop needs zero extra tooling — no ts-node, no nodemon, no tsx.

tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "lib": ["ES2021"],
    "types": ["node"],
    "outDir": "./dist",
    "rootDir": "./src",
    "incremental": true,
    "erasableSyntaxOnly": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

If this lives in its own repo instead of the monorepo, drop the extends line and inline the options from the root tsconfig.base.json above.

package.json scripts

{
  "scripts": {
    "dev": "node --watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "typecheck": "tsc --noEmit"
  }
}

What Changed vs. the NestJS Config

Option NestJS Plain Express Why
emitDecoratorMetadata ✅ ❌ removed Only Nest's DI needs runtime reflection
experimentalDecorators ✅ ❌ removed Same — no decorators in plain Express
erasableSyntaxOnly — ✅ added Keeps dev (native strip) and build (tsc) in sync

Everything else — module: nodenext, moduleResolution: nodenext, lib: ["ES2021"], types: ["node"], outDir, rootDir, incremental — carries over unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment