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

177 lines
6.6 KiB
Markdown

# 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
```text
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.
```ts
// 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).
```ts
// 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.
```dockerfile
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
```bash
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.