Files
development-monorepo-pnpm-3…/references/applications-frontend.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

7.7 KiB

Frontend Applications

Everything under applications-frontend/*: React application layout, IBM Carbon Design setup, Module Federation wiring, and the nginx container image.

Read this when adding an application, a page, a component or a hook; theming or styling with Carbon; wiring a host to a remote; or debugging a runtime module-loading failure.

Default stack: React with IBM Carbon Design (@carbon/react). If the project architecture specifies otherwise, follow the architecture — the layout, federation, and image rules below remain framework-neutral.


1. Application workspace layout

applications-frontend/<app-name>/
├── src/
│   ├── index.tsx              # entry — mounts React, imports Carbon styles
│   ├── bootstrap.tsx          # async boundary, required when federated (§4)
│   ├── App.tsx
│   ├── pages/<Page>/
│   ├── components/<Component>/    # app-local components only
│   ├── hooks/
│   ├── api/                   # typed clients; imports contract types type-only
│   └── styles/
│       └── _carbon.scss       # single Carbon import + theme configuration
├── public/
├── Dockerfile
├── nginx.conf
├── package.json
├── tsconfig.json
└── vite.config.ts | webpack.config.cjs   # webpack when Module Federation is used

A component belongs in packages-frontend/* only when a second application needs it and its API has settled. Until then it stays app-local — a shared component that is still changing imposes its churn on every consumer.

2. IBM Carbon Design

Install @carbon/react in the application workspace, never at the root.

Import Carbon styles once, in one SCSS entry point. Importing them per-component multiplies bundle size and produces unpredictable cascade order:

// src/styles/_carbon.scss
@use "@carbon/react" with (
  $font-path: "@ibm/plex"
);

Rules that keep a Carbon application coherent:

  • Use Carbon components before writing your own. A hand-rolled button or modal loses Carbon's keyboard handling, focus management, and ARIA wiring — accessibility work that is already done and easy to get wrong.
  • Theme with Carbon tokens, never raw hex. $background, $text-primary, $layer-01. A raw colour is invisible to theme switching and breaks the moment someone enables dark mode.
  • Use the Carbon grid (Grid, Column) rather than ad-hoc flexbox for page structure, so breakpoints match the rest of the system.
  • Never mix in a second component library. Two design systems in one app means two type scales, two grids, and tokens that fight each other.
  • Follow Carbon's spacing scale ($spacing-01 … $spacing-13) instead of arbitrary pixels.

Theme selection belongs at the application root via Carbon's Theme component, so a switch propagates to every component without per-component overrides.

3. Talking to backend services

API clients live in src/api/. Import contract types type-only from the owning backend package — a value import pulls server code into the browser bundle and nothing in the build will flag it (SKILL.md §3):

import type { UserDto } from "@<scope>/user-contracts";

export async function fetchUser(id: string): Promise<UserDto> {
  const res = await fetch(`${BASE_PATH}/users/${id}`);
  if (!res.ok) throw new Error(`fetchUser failed: ${res.status}`);
  return res.json() as Promise<UserDto>;
}

Base paths come from configuration, never hardcoded per call site — when a service's base path changes, one constant should move, not twenty fetch calls.

4. Module Federation

Used when applications are composed at runtime: a host loads remotes over HTTP. Both are built and deployed independently, so nothing catches a mismatch at compile time. The contract has three parts, and all three must agree:

  1. the remote's URL and its remoteEntry.js filename,
  2. the exact keys in the remote's exposes, matched by the host's imports,
  3. the shared-dependency singleton settings on both sides.
// remote — webpack.config.cjs
new ModuleFederationPlugin({
  name: "remoteApp",
  filename: "remoteEntry.js",
  exposes: { "./Widget": "./src/components/Widget" },
  shared: { react: { singleton: true, requiredVersion: deps.react },
            "react-dom": { singleton: true, requiredVersion: deps["react-dom"] } },
});

// host — webpack.config.cjs
new ModuleFederationPlugin({
  name: "hostApp",
  remotes: { remoteApp: "remoteApp@https://<host>/remoteEntry.js" },
  shared: { react: { singleton: true, requiredVersion: deps.react },
            "react-dom": { singleton: true, requiredVersion: deps["react-dom"] } },
});

The async bootstrap is mandatory. src/index.tsx must do nothing but import("./bootstrap"). Federated modules resolve asynchronously; importing React directly in the entry file evaluates it before the shared scope is initialised, and you get two React copies with hook errors that look nothing like a configuration problem.

Failure modes and what they actually mean:

Symptom Cause
Shared module is not available for eager consumption missing async bootstrap
Invalid hook call / two React copies singleton: true missing on one side
ScriptExternalLoadError wrong remote URL, or the remote is not deployed
Host builds, blank page at runtime exposes key renamed on the remote only

Changing any part of the contract is a breaking change for the other side (SKILL.md §8).

5. Container image

Build with turbo prune, then serve the static output from nginx:

FROM node:22-alpine AS pruner
WORKDIR /app
RUN corepack enable
COPY . .
RUN pnpm dlx turbo prune --scope=@<scope>/<app> --docker

FROM node:22-alpine AS builder
WORKDIR /app
RUN corepack enable
COPY --from=pruner /app/out/json/ .
RUN pnpm install --frozen-lockfile
COPY --from=pruner /app/out/full/ .
RUN pnpm turbo run build --filter=@<scope>/<app>

FROM nginx:alpine AS runner
COPY --from=builder /app/applications-frontend/<app>/dist /usr/share/nginx/html
COPY applications-frontend/<app>/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8080

nginx.conf must route unknown paths back to index.html, or a client-side route will 404 on refresh and deep links will not work:

location / {
  try_files $uri $uri/ /index.html;
}

Anything baked in at build time is public. Build-time environment variables are compiled into the bundle — never put a secret in one.

6. Validation

pnpm check-types --filter="@<scope>/<app>"
pnpm lint        --filter="@<scope>/<app>"
pnpm build       --filter="@<scope>/<app>"

Federated applications additionally require host and remote running together — separate green builds prove nothing about a runtime contract:

pnpm dev --filter="@<scope>/<remote>"
pnpm dev --filter="@<scope>/<host>"

Then load the host and confirm the remote's component actually renders, with a clean console.

7. Mistakes to avoid

  • Importing backend contract types with a value import instead of import type.
  • Importing Carbon styles in more than one place.
  • Raw hex colours or arbitrary pixel spacing instead of Carbon tokens and the spacing scale.
  • Hand-rolling a component Carbon already provides.
  • Mixing a second component library into a Carbon application.
  • Omitting the async bootstrap in a federated application.
  • Changing an exposes key, remote name, or remote URL on one side only.
  • Setting singleton: true on one side of a shared dependency but not the other.
  • Missing the try_files fallback in nginx.conf.
  • Baking a secret into a build-time environment variable.
  • Promoting an app-local component into packages-frontend/* before a second consumer exists.