# 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 ```text applications-frontend// ├── src/ │ ├── index.tsx # entry — mounts React, imports Carbon styles │ ├── bootstrap.tsx # async boundary, required when federated (§4) │ ├── App.tsx │ ├── pages// │ ├── components// # 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: ```scss // 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): ```ts import type { UserDto } from "@/user-contracts"; export async function fetchUser(id: string): Promise { const res = await fetch(`${BASE_PATH}/users/${id}`); if (!res.ok) throw new Error(`fetchUser failed: ${res.status}`); return res.json() as Promise; } ``` 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. ```js // 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:///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: ```dockerfile FROM node:22-alpine AS pruner WORKDIR /app RUN corepack enable COPY . . RUN pnpm dlx turbo prune --scope=@/ --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=@/ FROM nginx:alpine AS runner COPY --from=builder /app/applications-frontend//dist /usr/share/nginx/html COPY applications-frontend//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: ```nginx 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 ```bash pnpm check-types --filter="@/" pnpm lint --filter="@/" pnpm build --filter="@/" ``` Federated applications additionally require host and remote **running together** — separate green builds prove nothing about a runtime contract: ```bash pnpm dev --filter="@/" pnpm dev --filter="@/" ``` 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.