Concern
Monorepo layout and workspace management.
Technology
pnpm workspaces (workspace layout and linking). The package manager and CLI (install, add, run, lockfile) are documented in package-manager.
Documentation
- Website: https://pnpm.io/workspaces
- Repository: https://github.com/pnpm/pnpm
- Getting started: https://pnpm.io/workspaces#adding-to-an-existing-project
Version
v10
Implementation
The boilerplate is a pnpm workspace with a root package.json and pnpm-workspace.yaml listing the backend, shared, frontends/shared, and desktop packages. The shared package holds code used by both backend and frontends (e.g. API response types, APP_NAME); frontends/shared holds frontend-only code (e.g. API client, cn utility, Logo component). Each has its own package.json, tsconfig.json, and src/ layout. Shared is used by backend and frontends (e.g. APP_NAME in the frontends/shared Logo component); at least one frontend renders a component from frontends-shared (e.g. Logo on the sign-in page and in the sidebar header).
Backend and desktop declare "shared": "workspace:*" and "frontends-shared": "workspace:*" so they consume the built output from node_modules without path mapping. The root dev script builds both shared packages once, then runs scripts/dev.js, which finds available ports (prefer 3000 for backend, 5173 for frontend), sets BACKEND_PORT and FRONTEND_DESKTOP_PORT only; the backend strips BACKEND_ to get PORT, and the desktop Vite config maps FRONTEND_DESKTOP_PORT to VITE_PORT and derives VITE_BACKEND_URL from BACKEND_PORT when unset. The script then starts the concurrent watch and dev processes with that env so multiple worktrees or other apps do not conflict. See stack rule development. The backend start:dev script uses nodemon to watch backend/src and shared/dist, so changes in shared code trigger a backend restart and frontends receive updates via Vite HMR.
Layout
- backend — NestJS + Typescript app (Node).
- frontends/desktop — Vite + React + TypeScript app (browser).
- shared — TypeScript package consumed by both backend and frontends. Contains code that must work in Node and in the browser (no Node-only or DOM-only APIs in shared).
- frontends/shared — TypeScript package consumed by frontends only (not backend). Can be frontend-specific (e.g. use DOM, browser APIs, or frontend-only types). Use for code shared between desktop and other frontends that the backend does not need.
Code sharing (shared package)
- Workspace dependency: Backend and each frontend depend on
sharedvia"shared": "workspace:*"in theirpackage.json. No TypeScript path mapping is used; resolution is vianode_modules(pnpm links the workspace package). - Build order: The shared package must be built (emits
dist/) before backend or frontend run or build. Usepnpm --filter shared run buildonce after install (e.g. during boilerplate regeneration). Rootpnpm buildruns all packages in dependency order, so shared is built first and no separate shared build is needed for a full build. - Consumption: Shared exposes ESM (see
static-typingfor TypeScript configuration). Backend (Node) and frontend (Vite) consume the built output (dist/). The package exposes a root entry (shared→dist/index.js), a single-segment subpath (shared/*→dist/*.js), and a multi-segment subpath (shared/*/*→dist/*/*.js) so entity directories under the models pattern are importable (e.g.shared/cat/model,shared/cat/create). Useimport { … } from 'shared'for root exports andimport { … } from 'shared/<name>'orshared/<entity>/<file>for modules; no need to re-export fromshared/src/index.tsor to add per-file entries inpackage.json. - Adding shared code: Add source under
shared/src/<name>.tsfor top-level modules (e.g.api-response) or undershared/src/<entity>/<file>.tsfor entity-scoped modules (see patternmodels). The build emits todist/accordingly; the package’s subpath patterns make it importable asshared/<name>orshared/<entity>/<file>by default; the sharedpackage.jsonmust include both"./*"and"./*/*"inexportsso entity directories resolve. Optionally re-export fromshared/src/index.tsif the module should also be available from the root barrel. Keep shared free of runtime dependencies on NestJS, React, or Vite.
Frontends-only shared (frontends/shared)
- Package name:
frontends-shared. Only frontends depend on it via"frontends-shared": "workspace:*"; backend must not depend on it. - Use for: UI helpers, frontend-only constants, shared React components (e.g. the boilerplate exports Logo from frontends/shared), shared hooks, browser/DOM utilities, or anything that would not run in Node. Can use
lib: ["DOM"],jsx: "react-jsx", and frontend-specific TypeScript options (seestatic-typing). List React as a peer dependency so the consuming frontend’s React is used. - Consumption: Same as
shared: root entry (frontends-shared→dist/index.js) and subpath patterns so any built module is importable without per-file exports. Useimport { … } from 'frontends-shared'for root exports andimport { … } from 'frontends-shared/<path>'for modules (e.g.frontends-shared/lib/api,frontends-shared/lib/utils). The package’sexportsuse./*(one segment) and./*/*(two segments) so new files underfrontends/shared/src/are importable by path by default. - Adding code: Add source under
frontends/shared/src/(e.g.src/lib/<name>.ts). No change topackage.jsonis required; the subpath patterns make it importable asfrontends-shared/lib/<name>(orfrontends-shared/<name>for top-level files). Optionally re-export fromsrc/index.tsif it should be on the root barrel. - Build and dev: Same pattern as
shared: emit todist/, providebuildandbuild:watch. Rootpnpm buildbuilds it (frontends depend on it); rootpnpm devruns its watch build so frontends see changes. Vite watches the resolved package and triggers HMR whenfrontends/shared/distupdates.
Hot reload (development)
When you run pnpm dev from the boilerplate root, changes in the shared package should trigger recompile and reload in both backend and frontends.
- Shared: Run
sharedin watch mode (pnpm --filter shared run build:watch) so thattsc --watchkeepsshared/dist/up to date when you editshared/src/. Similarly, frontends/shared runsbuild:watch. The rootdevscript runs a one-time build of both shared packages then scripts/dev.js (finds available ports, sets env, spawns concurrently). Development is eased by the automatic dev command and central configuration; see stack rule development. - Backend: The backend
start:devscript uses nodemon to watchbackend/srcandshared/dist. When files in either change, nodemon restartsnest start, so the backend recompiles and reloads with the latest shared code. - Frontend: Vite watches resolved dependencies. Because the app imports
shared, Vite watches the resolved path (the shared package output). Whenshared/dist/is updated by shared’s watch build, Vite detects the change and triggers HMR/reload.
Integration
package-manager / pnpm
Root and workspace commands (pnpm install, pnpm add, pnpm run, pnpm -r, pnpm --filter) and the lockfile are defined in package-manager. This rule covers only workspace layout, shared packages, and build/dev orchestration.