--- name: development-monorepo-pnpm description: > Create, review, refactor, debug, and maintain full-stack monorepos built on pnpm workspaces and Turborepo, split four ways — applications-backend, applications-frontend, packages-backend, packages-frontend. Backend services default to NestJS; browser applications default to React with IBM Carbon Design, unless the project architecture states otherwise. Use when adding or changing a service, controller, module, React page, component or hook; assigning ports and API base paths; sharing API contract types across the front/back boundary; wiring Module Federation; editing shared eslint or typescript presets; cutting changesets; editing Dockerfiles, Helm charts, or Turborepo tasks; running filtered validation or blast-radius checks; or fixing workspace, dependency, lockfile, or build-pipeline problems — even when the user only names their repository instead of saying "monorepo". license: Proprietary metadata: author: workspace-skills-code-agent version: "1.0" spec: agentskills.io/specification compatibility: Requires pnpm (via corepack) and Node. Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agent environments --- # Full-Stack pnpm + Turborepo Monorepo Work rules for monorepos holding **both** backend services and browser applications, using **pnpm workspaces** for dependency resolution and **Turborepo** for task orchestration. The defining choice in this layout is that the front/back split is expressed in the **directory names themselves**, not in a nested convention. Four top-level workspace roots, each with one job. ## Scope routing Pick the reference that matches the change; do not load all of them. | The change touches | Read | |---|---| | A service under `applications-backend/*` — modules, controllers, providers, config | `references/applications-backend.md` | | An app under `applications-frontend/*` — components, routing, Carbon, Module Federation | `references/applications-frontend.md` | | Anything under `packages-backend/*` or `packages-frontend/*` — exports, presets, changesets | `references/packages.md` | | `pnpm-workspace.yaml`, `turbo.json`, root scripts, formatting, dependency overrides | `references/workspace-and-tooling.md` | | Dockerfiles, images, Helm charts, values, deploy automation | `references/containers-and-helm.md` | ## 1. Canonical repository shape ```text / ├── applications-backend/ # deployable services — NestJS by default │ └── / ├── applications-frontend/ # deployable browser apps — React + Carbon by default │ └── / ├── packages-backend/ # libraries consumed by services │ └── / ├── packages-frontend/ # libraries consumed by browser apps │ └── / ├── helm-charts// # deployment assets ├── .scripts/ # shell modules: docker, images, helm, pipelines ├── package.json # root scripts + packageManager + engines + overrides ├── pnpm-workspace.yaml # the four globs below ├── turbo.json # task pipeline ├── pnpm-lock.yaml ├── .prettierrc # single formatting authority └── Taskfile.yml # thin delegator — includes .scripts/ modules only ``` ```yaml # pnpm-workspace.yaml packages: - "applications-backend/*" - "applications-frontend/*" - "packages-backend/*" - "packages-frontend/*" ``` Applications sit **directly** under their root — `applications-backend/`, not `applications-backend//core`. The four-way split already gives each side its own namespace, so no extra nesting layer is needed. **A workspace exists only if a glob matches it.** Creating a directory is not enough: confirm the glob, then run `pnpm install`. A directory outside these four roots is not part of the build graph, and Turborepo will silently ignore it. ### Why the split is in the directory name One glance at a path tells you which runtime a file ends up in. That matters because the two sides have genuinely different constraints — a browser bundle ships to untrusted clients and pays for every kilobyte, a service does not; a service holds credentials, a bundle must never. When both live under one `packages/` root, the only thing standing between a secret-handling utility and a browser bundle is someone reading the import path carefully. ## 2. Default stacks | Layer | Default | Applies unless | |---|---|---| | `applications-backend/*` | **NestJS** — modules, controllers, providers, DI | the project architecture specifies another framework | | `applications-frontend/*` | **React** with **IBM Carbon Design** (`@carbon/react`) | the project architecture specifies another framework or design system | These are defaults, not mandates. **The project's stated architecture always wins** — read the paired code-agent repository's `memory-bank/` (§9) and any architecture decision record *before* scaffolding. If the architecture is silent, use the defaults above and record the choice; if it disagrees, follow it and do not "correct" it toward these defaults. Do not mix design systems inside one frontend application. Carbon components and a second component library in the same app produce two type scales, two grid systems, and a token set that fights itself. ## 3. The one sanctioned cross-boundary dependency Frontend workspaces must not import from `packages-backend/*`, and backend workspaces must not import from `packages-frontend/*` — with exactly one exception: **API contract types.** The service that defines an endpoint owns its contract, so contract packages live under `packages-backend/-contracts` and expose **types only** — no runtime code, no dependencies, no side effects. Frontend consumers import them type-only: ```ts import type { UserDto } from "@/user-contracts"; ``` `import type` is erased at compile time, so nothing from the backend package reaches the browser bundle. A plain `import` from the same package does reach it — that is the failure this rule exists to prevent, and it will not show up as a type error. A contract package that acquires a runtime dependency has stopped being a contract package. Split it before it ships anything into a bundle. ## 4. Tooling baseline Root `package.json` pins the contract for every contributor and CI runner: `packageManager` (exact pnpm version, read by corepack), `engines.node`, `pnpm.overrides` for security pins, and root scripts that always delegate to Turborepo — never to a single workspace. ```bash pnpm build # turbo run build pnpm dev # turbo run dev pnpm lint # turbo run lint pnpm check-types # turbo run check-types pnpm format # prettier --write pnpm publish-packages # build:ci + lint, then changeset version && changeset publish pnpm prepush # turbo build --filter=...[HEAD^] — only what the branch touched ``` `build` depends on `^build`, `check-types`, and `lint`, so a type error or lint error is a build failure for every downstream workspace, not a warning. Full pipeline, cache outputs, and override rules → `references/workspace-and-tooling.md`. ## 5. Before making changes 1. Identify the exact workspace path, and therefore which of the four roots you are in. 2. Read that workspace's `package.json` — scripts, dependencies, whether it is publishable. 3. Inspect its `tsconfig.json`, lint config, build config, and source layout before adding. 4. Map what it consumes from shared packages, and what consumes it. 5. For Module Federation changes, map the host↔remote contract before editing either side. 6. Read the paired code-agent repository's `rules/` and `memory-bank/` when one exists (§9) — it carries architecture decisions the code does not. ```bash find applications-backend applications-frontend -maxdepth 2 -name package.json -print find packages-backend packages-frontend -maxdepth 2 -name package.json -print pnpm list --depth 0 task --list ``` ## 6. Implementation rules ### Workspace boundaries - Application logic stays inside its own workspace. - Promote into a shared package only when **more than one** application needs it *and* the abstraction has stopped changing. Config presets are the exception — shared from day one. Premature promotion is the most expensive mistake here: every consumer inherits the churn. - A shared package goes in the root matching its **consumers**, never its author. - Never cross a workspace boundary with a relative import (`../../other-app/src`). Depend on the workspace package, or the Module Federation contract, or not at all. ### TypeScript - Every workspace extends a shared config from the `typescript-config` preset. Do not restate `compilerOptions` locally beyond `outDir`, `rootDir`, `include`, `exclude`. - `check-types` is `tsc --noEmit`, named identically in every workspace so Turborepo can run it across the graph. - Keep types explicit at public boundaries; avoid broad `any`. ### Dependencies - Add a dependency to the **smallest** workspace that needs it. - Root dependencies are monorepo tooling only — turbo, prettier, husky, changesets, typescript. Nothing an application imports at runtime. - Inter-workspace dependencies use `workspace:*`. - Run `pnpm install` after every `package.json` edit and commit the lockfile change with the edit that caused it. - Never remove a `pnpm.overrides` entry without a security review — removing one silently reintroduces a vulnerable transitive version and nothing in the build complains. ### Formatting Root `.prettierrc` is the single authority. Do not add per-package Prettier config; a second config produces churn diffs every time someone formats a file from the other side. ## 7. Validation Validate the narrowest scope covering the change, then widen if it touched shared code. ```bash # Single workspace pnpm check-types --filter="@/" pnpm lint --filter="@/" pnpm build --filter="@/" # Shared package or cross-workspace change — unfiltered, plus each consumer pnpm check-types && pnpm lint && pnpm build pnpm build --filter="@/" # Only what the branch changed pnpm prepush ``` Module Federation changes additionally require **host and remote running together** — a green build on each side separately proves nothing about a runtime contract: ```bash pnpm dev --filter="@/" pnpm dev --filter="@/" ``` Record the commands run and their outcome in the task record (§9). "It builds", without the filter you used, is not a validation result. ## 8. Blast-radius checks Before changing anything another workspace can observe — an exported symbol, a REST base path, a port, an environment variable name, an MFE `exposes` key: 1. Enumerate the surface. 2. Search every consumer across all four roots. 3. Decide explicitly whether the change is backward-compatible. 4. If it is not: update consumers in the same change, or ship a compatible wrapper first. 5. Validate each affected consumer with its own `--filter` build. ```bash grep -Rn "from '@/'" applications-backend applications-frontend \ packages-backend packages-frontend grep -Rn "api/" applications-frontend # base-path callers ``` A port or base path change is never local: it also lands in the Helm chart values, the gateway routing, and every frontend caller. See `references/containers-and-helm.md`. ## 9. Durable records Monorepos in this family pair with a `-code-agent/` repository holding `rules/`, `memory-bank/`, and `tasks/`. Update `memory-bank/` when durable facts change — application inventory, ports, base paths, MFE topology, chosen frameworks and design system, chart structure. Create a task record under `tasks/` for substantial work. Never place task records inside the target repository unless explicitly asked. ## 10. Common mistakes to avoid - Importing a `packages-backend/*` package from a frontend workspace with a value import instead of `import type` — it silently pulls server code into the browser bundle. - Putting a shared package in the root that matches its author rather than its consumers. - Adding a workspace outside the four roots, or without updating `pnpm-workspace.yaml`. - Re-introducing a `core/` nesting layer under an application directory. - Changing a service port or base path without updating Helm values, gateway routing, and every caller. - Changing an MFE `exposes` key, remote name, or remote URL on one side only. - Removing a `pnpm.overrides` entry without a security review. - Adding a root-level dependency for a need local to one workspace. - Skipping `--filter` validation for the exact workspace you edited. - Mixing a second component library into a Carbon application. - Overriding the project's stated architecture because it differs from the defaults in §2. - Adding tasks directly to root `Taskfile.yml` instead of a `.scripts/` module. - Editing generated artifacts (`dist/`, rendered manifests, lockfiles) by hand. - Committing `.env`, `key.pem`, `cert.pem`, registry tokens, or `.npmrc` secrets — including into memory-bank or task documentation. ## Reference files | File | Contents | Read when | |---|---|---| | `references/applications-backend.md` | NestJS service layout, modules/controllers/providers, config and env, ports and base paths, health checks, service Dockerfile | Touching `applications-backend/*` | | `references/applications-frontend.md` | React app layout, IBM Carbon setup and theming, routing, state, Module Federation wiring and failure modes, nginx image | Touching `applications-frontend/*` | | `references/packages.md` | Package tiers per root, contract packages, public-API safety, export and dependency rules, changesets and publishing | Touching `packages-backend/*` or `packages-frontend/*` | | `references/workspace-and-tooling.md` | Workspace globs, `turbo.json` pipeline and caching, root scripts, Prettier, `pnpm.overrides`, lockfile discipline | Touching root config or the task pipeline | | `references/containers-and-helm.md` | `turbo prune` image builds for both sides, chart layout, values and secrets, port/base-path propagation | Touching Dockerfiles, images, or charts | | `assets/example-shared-package/` | Minimal shared TypeScript package — ESM, barrel export, shared tsconfig and eslint | Scaffolding a package under either packages root |