# 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// ├── 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 │ └── / │ ├── .module.ts │ ├── .controller.ts # HTTP surface only │ ├── .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/", 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/-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=@/ --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 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 CMD ["node", "applications-backend//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="@/" pnpm lint --filter="@/" pnpm build --filter="@/" pnpm dev --filter="@/" # it must actually boot curl -s localhost://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.