Files
development-monorepo-pnpm-3…/references/packages.md
root-at-skicandClaude Opus 5 ff69bcca2f development-monorepo-pnpm skill: initial import
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>
2026-08-12 23:49:02 +03:00

5.5 KiB

Shared Packages

Everything under packages-backend/* and packages-frontend/*: which root a package belongs in, package tiers, public-API safety, and changesets publishing.

Read this when creating a shared package, changing one's exports or dependencies, deciding where a package belongs, or cutting a release.


1. Choosing the root

A package lives in the root that matches its consumers, never its author. A utility extracted from a service but used only by browser applications belongs in packages-frontend/*.

Consumed by Root
backend services only packages-backend/*
browser applications only packages-frontend/*
both — API contract types packages-backend/<domain>-contracts, types only (SKILL.md §3)
both — genuine runtime code see below

Truly shared runtime code is rarer than it looks. Before creating one, check that it is free of Node built-ins (fs, path, crypto), free of DOM APIs, and free of anything environment-specific. If it is not, you have two packages, not one — and forcing it into a single package pulls one environment's assumptions into the other.

When such a package is justified, place it under the root of its primary consumer, keep it dependency-free, and document the constraint at the top of its README. A dependency added later is what breaks the other side.

2. Package tiers

Tier Purpose Examples
Configuration presets shared build/lint/type config eslint-config, typescript-config
Contracts API types shared across the boundary <domain>-contracts
Domain libraries business logic for one domain billing-core, user-core
Utilities domain-independent helpers date-utils, result

Configuration presets are shared from day one — that is their job. Every other tier earns its existence only after a second consumer appears.

3. Package anatomy

packages-<side>/<package-name>/
├── src/
│   ├── index.ts        # barrel — the entire public API
│   └── <module>.ts
├── package.json
├── tsconfig.json
├── .eslintrc.cjs | eslint.config.js
└── README.md           # what it is, who consumes it, constraints
{
  "name": "@<scope>/<package-name>",
  "version": "0.1.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
  "scripts": {
    "build": "tsc",
    "check-types": "tsc --noEmit",
    "lint": "eslint src"
  }
}

src/index.ts is the public API. Anything not exported from the barrel is internal and may change freely; anything exported is a contract with every consumer. Deep imports into dist/ or src/ from a consumer defeat this — treat one as a bug in the consumer.

Script names must match the tasks in turbo.json, or Turborepo silently skips the workspace.

4. Changing a public API

Adding an export is safe. Changing or removing one is not.

  1. Search every consumer across all four roots before editing:
grep -Rn "from '@<scope>/<package>'" applications-backend applications-frontend \
                                     packages-backend packages-frontend
  1. Decide explicitly whether the change is backward-compatible.
  2. If it is not: update consumers in the same change, or add the new export alongside the old one, migrate, then remove.
  3. Build every affected consumer with its own --filter.

A shared package's dependencies propagate to every consumer, so weigh each one twice. This matters most in packages-frontend/*, where a dependency lands in a browser bundle that ships to users.

5. Configuration presets

typescript-config and eslint-config exist so rules live in one place. Every workspace extends them rather than restating compilerOptions or lint rules locally.

Their blast radius is the whole repository — a rule added to the shared ESLint preset can fail the build in workspaces you never opened. Change one, then run an unfiltered pnpm lint && pnpm check-types before committing.

Each side may need its own preset variant (a DOM lib for frontend, Node types for backend). Keep the shared base shared and put only the genuine differences in the variant.

6. Changesets and publishing

pnpm changeset            # describe the change, pick the bump
pnpm publish-packages     # build:ci + lint, then version and publish
  • One changeset per meaningful change, written for a consumer deciding whether to upgrade — not a restatement of the commit message.
  • A breaking change to an exported API is a major bump, even if every current consumer lives in this repository.
  • Never hand-edit versions or CHANGELOG.md; changesets owns both.
  • Internal-only packages are marked "private": true and are never published.

7. Mistakes to avoid

  • Putting a package in the root matching its author instead of its consumers.
  • Creating a "shared" runtime package that imports Node built-ins or DOM APIs.
  • Giving a contract package a runtime dependency.
  • Exporting something from the barrel that was meant to be internal.
  • Deep-importing another package's src/ or dist/ from a consumer.
  • Changing an exported API without searching consumers first.
  • Adding a dependency to a frontend package without considering bundle size.
  • Restating compilerOptions or lint rules locally instead of changing the shared preset.
  • Editing versions or changelogs by hand.
  • Promoting logic into a shared package before a second consumer exists.