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>
177 lines
6.6 KiB
Markdown
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.
|