Full-stack pnpm + Turborepo monorepo skill, independent of the separate backend and frontend monorepo skills it draws on. Repository shape is split four ways at the top level: applications-backend, applications-frontend, packages-backend, packages-frontend — so a path states which runtime a file ends up in. Defaults: NestJS for backend services, React with IBM Carbon Design for browser applications; the project's stated architecture overrides both. Covers workspace globs, the Turborepo pipeline, filtered validation, blast-radius checks, Module Federation, turbo prune image builds, and Helm. Documents one sanctioned cross-boundary dependency: API contract types owned by the backend, imported type-only by the frontend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
14 KiB
name, description, license, metadata, compatibility
| name | description | license | metadata | compatibility | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| development-monorepo-pnpm | 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". | Proprietary |
|
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
<repo>/
├── applications-backend/ # deployable services — NestJS by default
│ └── <service-name>/
├── applications-frontend/ # deployable browser apps — React + Carbon by default
│ └── <app-name>/
├── packages-backend/ # libraries consumed by services
│ └── <package-name>/
├── packages-frontend/ # libraries consumed by browser apps
│ └── <package-name>/
├── helm-charts/<chart-name>/ # 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
# pnpm-workspace.yaml
packages:
- "applications-backend/*"
- "applications-frontend/*"
- "packages-backend/*"
- "packages-frontend/*"
Applications sit directly under their root — applications-backend/<name>, not
applications-backend/<name>/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/<domain>-contracts and expose types only — no
runtime code, no dependencies, no side effects. Frontend consumers import them type-only:
import type { UserDto } from "@<scope>/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.
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
- Identify the exact workspace path, and therefore which of the four roots you are in.
- Read that workspace's
package.json— scripts, dependencies, whether it is publishable. - Inspect its
tsconfig.json, lint config, build config, and source layout before adding. - Map what it consumes from shared packages, and what consumes it.
- For Module Federation changes, map the host↔remote contract before editing either side.
- Read the paired code-agent repository's
rules/andmemory-bank/when one exists (§9) — it carries architecture decisions the code does not.
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-configpreset. Do not restatecompilerOptionslocally beyondoutDir,rootDir,include,exclude. check-typesistsc --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 installafter everypackage.jsonedit and commit the lockfile change with the edit that caused it. - Never remove a
pnpm.overridesentry 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.
# Single workspace
pnpm check-types --filter="@<scope>/<name>"
pnpm lint --filter="@<scope>/<name>"
pnpm build --filter="@<scope>/<name>"
# Shared package or cross-workspace change — unfiltered, plus each consumer
pnpm check-types && pnpm lint && pnpm build
pnpm build --filter="@<scope>/<consumer>"
# 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:
pnpm dev --filter="@<scope>/<host>"
pnpm dev --filter="@<scope>/<remote>"
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:
- Enumerate the surface.
- Search every consumer across all four roots.
- Decide explicitly whether the change is backward-compatible.
- If it is not: update consumers in the same change, or ship a compatible wrapper first.
- Validate each affected consumer with its own
--filterbuild.
grep -Rn "from '@<scope>/<package>'" applications-backend applications-frontend \
packages-backend packages-frontend
grep -Rn "api/<name>" 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 <repo>-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 ofimport 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
exposeskey, remote name, or remote URL on one side only. - Removing a
pnpm.overridesentry without a security review. - Adding a root-level dependency for a need local to one workspace.
- Skipping
--filtervalidation 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.ymlinstead of a.scripts/module. - Editing generated artifacts (
dist/, rendered manifests, lockfiles) by hand. - Committing
.env,key.pem,cert.pem, registry tokens, or.npmrcsecrets — 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 |