Files
development-monorepo-pnpm-3…/SKILL.md

16 KiB

name, description, license, metadata, compatibility
name description license metadata compatibility
development-monorepo-pnpm--3darch 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
author version spec
workspace-skills-code-agent 1.0 agentskills.io/specification
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

3D Architecture Wizzard Project Adoption

This is the primary project-adopted skill for 3D Architecture Wizzard (3darch).

Project engineering peers:

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

  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.
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.

# 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:

  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.
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 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