Files
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

6.6 KiB

Backend Applications

Everything under applications-backend/*: NestJS service layout, configuration, ports and base paths, health checks, and the service container image.

Read this when adding a service, adding an endpoint to one, changing configuration or environment variables, assigning a port or base path, or debugging a service that will not start.

Default framework: NestJS. If the project architecture specifies otherwise, follow the architecture — the layout rules below that are framework-neutral still apply.


1. Service workspace layout

applications-backend/<service-name>/
├── src/
│   ├── main.ts                   # bootstrap — the only file that binds a port
│   ├── app.module.ts             # root module, imports feature modules
│   ├── config/
│   │   ├── configuration.ts      # typed config factory, reads process.env once
│   │   └── validation.ts         # schema; process exits if the env is invalid
│   ├── health/                   # liveness and readiness endpoints
│   └── <feature>/
│       ├── <feature>.module.ts
│       ├── <feature>.controller.ts    # HTTP surface only
│       ├── <feature>.service.ts       # business logic
│       ├── dto/                       # request/response shapes
│       └── entities/
├── test/
├── Dockerfile
├── nest-cli.json
├── package.json
├── tsconfig.json
└── tsconfig.build.json

One responsibility per layer, and the boundary is enforceable by review:

Layer Holds Never holds
*.controller.ts routing, param binding, response shape business rules, direct DB access
*.service.ts business logic, orchestration HTTP concerns, @Res(), status codes
dto/ validated request/response shapes logic
entities/ persistence models HTTP or transport concerns

A controller that reaches past its service into a repository is the most common drift in this layout, and it is invisible until you try to reuse the logic from a queue consumer or a cron job and discover it only exists inside an HTTP handler.

2. Configuration and environment

Read process.env in one place — the config factory — and inject typed config everywhere else. Scattered process.env reads make it impossible to know a service's full input surface, which is exactly what you need when writing the Helm values.

// src/config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT ?? "3000", 10),
  basePath: process.env.API_BASE_PATH ?? "/api/<service-name>",
  database: { url: process.env.DATABASE_URL },
});

Validate at startup and fail fast. A service that boots with a missing variable and fails on the first request that needs it turns a config error into a production incident, and the stack trace points at the wrong place.

Every variable added here must also be added to the Helm values and the service's documented env surface — see references/containers-and-helm.md.

3. Ports and base paths

Both are cross-cutting contracts, not local settings:

  • The port appears in main.ts, the Dockerfile EXPOSE, the Helm values, and the service's Kubernetes Service definition.
  • The base path appears in main.ts (app.setGlobalPrefix()), the gateway or ingress routing, and every frontend caller.

Assign a port from the repository's documented range and record it in the paired code-agent memory-bank/ — that record is what stops the next service from colliding with it. Changing either value is a blast-radius change (SKILL.md §8).

// src/main.ts
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix(config.basePath);
app.enableCors({ origin: config.corsOrigins });
await app.listen(config.port);

4. Health checks

Expose two endpoints, and keep them different:

  • Liveness — is the process alive? No dependency checks. A liveness probe that checks the database restarts a healthy service every time the database blips, turning a partial outage into a total one.
  • Readiness — can it serve traffic now? Checks dependencies it genuinely needs.

5. Consuming shared packages

Backend services consume packages-backend/* with workspace:*. They must never import from packages-frontend/*.

A service that defines an API contract publishes it as packages-backend/<domain>-contracts, types only, so frontend applications can import it type-only — see SKILL.md §3. Adding a runtime dependency to a contract package breaks that guarantee for every frontend consumer.

6. Container image

Multi-stage build using turbo prune to produce a minimal snapshot of just this service and its workspace dependencies. Pruning is what stops an unrelated workspace change from invalidating this image's layers.

FROM node:22-alpine AS pruner
WORKDIR /app
RUN corepack enable
COPY . .
RUN pnpm dlx turbo prune --scope=@<scope>/<service> --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>/<service>

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder --chown=app:app /app .
USER app
EXPOSE <port>
CMD ["node", "applications-backend/<service>/dist/main.js"]

Points that matter:

  • out/json/ is copied and installed before out/full/ so dependency installation caches independently of source changes.
  • --frozen-lockfile makes the build fail rather than silently resolve different versions.
  • Run as a non-root user. The default is root, and nothing warns you.

7. Validation

pnpm check-types --filter="@<scope>/<service>"
pnpm lint        --filter="@<scope>/<service>"
pnpm build       --filter="@<scope>/<service>"
pnpm dev         --filter="@<scope>/<service>"    # it must actually boot

curl -s localhost:<port>/<basePath>/health | jq .

A build that passes proves the types line up; only starting the service proves the config validation and port binding do.

8. Mistakes to avoid

  • Reading process.env outside the config factory.
  • Booting successfully with invalid configuration instead of failing fast.
  • A liveness probe that checks dependencies.
  • Business logic in a controller.
  • Importing from packages-frontend/*.
  • Giving a contract package a runtime dependency.
  • Changing a port or base path without updating Helm values, gateway routing, and callers.
  • Hardcoding a base path string in a frontend caller instead of deriving it from the contract.
  • Running the container as root.