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>
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 DockerfileEXPOSE, 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 beforeout/full/so dependency installation caches independently of source changes.--frozen-lockfilemakes 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.envoutside 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.