Skip to main content

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

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 shared via "shared": "workspace:*" in their package.json. No TypeScript path mapping is used; resolution is via node_modules (pnpm links the workspace package).
  • Build order: The shared package must be built (emits dist/) before backend or frontend run or build. Use pnpm --filter shared run build once after install (e.g. during boilerplate regeneration). Root pnpm build runs 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-typing for TypeScript configuration). Backend (Node) and frontend (Vite) consume the built output (dist/). The package exposes a root entry (shareddist/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). Use import { … } from 'shared' for root exports and import { … } from 'shared/<name>' or shared/<entity>/<file> for modules; no need to re-export from shared/src/index.ts or to add per-file entries in package.json.
  • Adding shared code: Add source under shared/src/<name>.ts for top-level modules (e.g. api-response) or under shared/src/<entity>/<file>.ts for entity-scoped modules (see pattern models). The build emits to dist/ accordingly; the package’s subpath patterns make it importable as shared/<name> or shared/<entity>/<file> by default; the shared package.json must include both "./*" and "./*/*" in exports so entity directories resolve. Optionally re-export from shared/src/index.ts if 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 (see static-typing). List React as a peer dependency so the consuming frontend’s React is used.
  • Consumption: Same as shared: root entry (frontends-shareddist/index.js) and subpath patterns so any built module is importable without per-file exports. Use import { … } from 'frontends-shared' for root exports and import { … } from 'frontends-shared/<path>' for modules (e.g. frontends-shared/lib/api, frontends-shared/lib/utils). The package’s exports use ./* (one segment) and ./*/* (two segments) so new files under frontends/shared/src/ are importable by path by default.
  • Adding code: Add source under frontends/shared/src/ (e.g. src/lib/<name>.ts). No change to package.json is required; the subpath patterns make it importable as frontends-shared/lib/<name> (or frontends-shared/<name> for top-level files). Optionally re-export from src/index.ts if it should be on the root barrel.
  • Build and dev: Same pattern as shared: emit to dist/, provide build and build:watch. Root pnpm build builds it (frontends depend on it); root pnpm dev runs its watch build so frontends see changes. Vite watches the resolved package and triggers HMR when frontends/shared/dist updates.

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 shared in watch mode (pnpm --filter shared run build:watch) so that tsc --watch keeps shared/dist/ up to date when you edit shared/src/. Similarly, frontends/shared runs build:watch. The root dev script 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:dev script uses nodemon to watch backend/src and shared/dist. When files in either change, nodemon restarts nest 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). When shared/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.