Concern
Static typing across the project: backend, frontends, and shared (types and interfaces used consistently).
Technology
TypeScript
Documentation
- Website: https://www.typescriptlang.org/
- Repository: https://github.com/microsoft/TypeScript
- Getting started: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html
Integration
Backend, frontends, and the shared package all use TypeScript. Shared package types are consumed by backend and frontends. Align with serialization (class-transformer DTOs) and validation (zod schemas) where shapes are shared across boundaries.
Version
v5
Implementation
The shared package uses a single shared/tsconfig.json that compiles to ESM with declaration files (outDir dist/), so both Node (backend) and the bundler (frontends) can consume it. The backend and each frontend get their own tsconfig from the Nest and shadcn scaffolds (nest new, shadcn create with Vite). Shared is built before any consumer runs; there is no TypeScript path mapping—backend and frontends resolve shared and frontends-shared via node_modules and the packages' exports in package.json.
TypeScript across the stack
Use the same TypeScript major version (v5) in backend, all frontends, and shared so type-checking is consistent.
Shared package
The shared package is a workspace package that compiles to ESM and declaration files so both backend (Node) and frontends (Vite/bundler) can use it.
- shared/tsconfig.json: Use
target: "ES2022",module: "ESNext",moduleResolution: "bundler",declaration: true,declarationMap: true,outDir: "./dist",rootDir: "./src". Do not includelib: ["DOM"]or JSX in shared; keep the package runtime-agnostic. Emit both JS and.d.ts(nonoEmit). - shared/package.json: Set
"type": "module","main": "./dist/index.js","types": "./dist/index.d.ts", and anexportsfield with the root (.→dist/index.js) and a subpath pattern (./*→dist/*.js) so any built module is importable asshared/<name>without per-file entries. Seemonorepofor adding shared modules. - After adding or changing shared, run
pnpm --filter shared run buildso consumers see up-to-date output. Rootpnpm buildbuilds shared first by dependency order.
Backend
Backend uses its own tsconfig (e.g. moduleResolution: "nodenext"). No tsconfig changes are required to consume shared; backend resolves shared via node_modules; types come from shared's types / exports. With moduleResolution: "nodenext", relative imports in backend source must use the .js extension (the runtime extension of the emitted file); e.g. from './mailing.service.js' not from './mailing.service'. Package imports (e.g. shared, @nestjs/...) stay extensionless.
Frontends
Each frontend uses its own tsconfig (e.g. with Vite/bundler resolution). No tsconfig path mapping for shared; frontends resolve shared from node_modules (workspace link). Ensure the frontend's tsconfig does not exclude the shared package; with the default include (e.g. ["src"]) and resolution via node_modules, types are found from shared's emitted .d.ts.
Background
Concepts that underpin the TypeScript configuration choices:
-
Modules — A module is a file that imports or exports; TypeScript and runtimes use module systems to resolve
import/exportand load code. The chosen system affects both how the compiler emits code and how Node or the bundler resolves package paths. -
ESM (ECMAScript modules) — The standard JavaScript module format:
import/exportsyntax, static structure, and asynchronous loading. Emitted asimport/exportin.jswhen TypeScriptmoduleis ESNext/ES2020+. Node supports ESM when the package has"type": "module"or the file is.mjs; bundlers (e.g. Vite) consume ESM natively. -
CommonJS (CJS) — The legacy Node module format:
require()andmodule.exports. TypeScript can emit CJS whenmoduleis CommonJS; Node runs it without"type": "module". Many older packages still use CJS, so interop (e.g.esModuleInterop) matters when the backend mixes ESM and CJS. -
Node — The backend runtime. It supports both ESM and CommonJS; resolution follows
package.jsonmain/exportsand (with the right Node/tsconfig) respectsexportsandtypes. UsingmoduleResolution: "nodenext"aligns the compiler with Node's actual resolution and ESM/CJS rules. -
Bundler — Frontend build tools (e.g. Vite) bundle sources and resolve imports at build time; they follow
package.jsonexportsand typically assume ESM.moduleResolution: "bundler"matches how they resolve paths and allows features like importing.tsortypesfromexports.
Configuration
TypeScript options that matter for backend, frontend, and shared to work together, in order of importance:
-
module and moduleResolution — Define how imports are resolved and what format is emitted.
- Shared:
module: "ESNext",moduleResolution: "bundler"so emitted JS is ESM and both Node and the bundler can consume it. - Backend:
module: "nodenext",moduleResolution: "nodenext"so the Nest app runs as Node ESM and correctly resolvessharedvia itspackage.jsonexports. - Frontend:
module: "ESNext",moduleResolution: "bundler"so Vite can bundle and resolvesharedfromnode_modules.
- Shared:
-
target — Controls the JavaScript language level of emitted code (e.g. ES2022); must be supported by both Node and the browser.
- Shared: use
"ES2022"(or same as the strictest consumer) so emitted code runs everywhere; keep at or below the lowest runtime target. - Backend: e.g.
"ES2023". - Frontend: e.g.
"ES2022"or higher.
- Shared: use
-
declaration and noEmit — Control whether the compiler emits
.d.tsfiles and whether it emits any JS at all; shared must emit both for consumers to use it.- Shared:
declaration: trueand do not setnoEmitso it emits both.jsand.d.tstooutDir; consumers rely on these. - Backend: typically emits (Nest builds to
dist). - Frontend: often
noEmit: truebecause Vite handles emit; types are still resolved from shared's.d.ts.
- Shared:
-
outDir and rootDir — Define where source lives and where compiled output is written; shared must align these with
package.jsonmain/types/exports.- Shared:
rootDir: "./src",outDir: "./dist"so the package entry isdist/index.jsand matchesmain/types/exportsinpackage.json. - Backend: each has its own; no need to reference shared's dirs.
- Frontend: each has its own; no need to reference shared's dirs.
- Shared:
-
lib — Declares which built-in APIs (e.g. DOM, ES2022) the type checker assumes exist; shared must avoid DOM so it stays runtime-agnostic.
- Shared:
["ES2022"]only; do not add"DOM"or browser-only libs so the package stays runtime-agnostic. - Backend: omit or use Node types via
@types/node. - Frontend: include
"DOM","DOM.Iterable"for browser APIs.
- Shared:
-
strict (and related flags) — Enable stricter type checking; shared should be at least strict so types are reliable for both consumers.
- Shared: use at least
strict: trueso types are reliable for both consumers; avoid relaxing. - Backend: can be stricter.
- Frontend: can be stricter.
- Shared: use at least
-
skipLibCheck — Skip type checking of declaration files in node_modules; use true everywhere to keep build times down.
- Shared:
trueto avoid type-checkingnode_modulesand keep build times down. - Backend:
true. - Frontend:
true.
- Shared:
-
resolvePackageJsonExports — Whether the compiler respects
package.jsonexportswhen resolving package paths; backend needs this for correct resolution ofshared.- Shared: follows bundler resolution.
- Backend: set
truewithmoduleResolution: "nodenext"so Node and the compiler respectshared'sexportsinpackage.json. - Frontend: follows bundler resolution.
-
esModuleInterop / allowSyntheticDefaultImports — Relax how default imports work with CommonJS modules; backend often needs these for Nest and CJS interop.
- Shared: optional; ESM-only code may not need them.
- Backend: often
truefor Nest and CommonJS interop. - Frontend: optional; ESM-only code may not need them.
-
experimentalDecorators / emitDecoratorMetadata — Enable decorator syntax and metadata emission; required for NestJS and class-transformer-style DTOs when used.
- Shared: only if you put decorators (e.g. class-transformer) in shared; otherwise omit.
- Backend: required for NestJS decorators.
- Frontend: only if using decorators there.
Hot reload (development)
For shared to trigger reloads in backend and frontends during pnpm dev: shared runs tsc --watch (build:watch); backend uses nodemon to watch shared/dist and restarts Nest when it changes; Vite watches the resolved shared output and triggers HMR. See monorepo for the full dev script and setup.