# Add Health Checks to a NestJS API

Yes. An agent adds liveness and readiness routes to your NestJS API with Terminus, checks the database in the ready route, tests both end to end and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/add-health-checks/nestjs/), in a Project that has your repository.

## What the agent does

The agent reads how the app boots before adding a module. In a NestJS API like `brocoders/nestjs-boilerplate`, `src/main.ts` creates the app on NestJS 11, adds Helmet and CORS, calls `enableShutdownHooks()`, sets a global prefix from `app.apiPrefix` with only `/` excluded, turns on URI versioning, and serves Swagger at `/docs`. `src/app.module.ts` picks the database at startup: a `<database-block>` comment wraps a ternary that loads `TypeOrmModule` for Postgres through `TypeOrmConfigService` or `MongooseModule` through `MongooseConfigService`, depending on `isDocumentDatabase` from `src/database/config/database.config.ts`. `src/home/home.controller.ts` already answers `GET /` with the app name, which is the closest thing to a health route today, and `package.json` lists no `@nestjs/terminus`.

It adds `@nestjs/terminus` at the version matching the installed `@nestjs/common` and a `HealthModule` under `src/health/`. `GET /health/live` answers 200 with no dependency checks, so an orchestrator restarts the process only when the event loop is stuck. `GET /health/ready` runs Terminus's `TypeOrmHealthIndicator.pingCheck` or `MongooseHealthIndicator.pingCheck`, chosen by the same `isDocumentDatabase` switch, with a short timeout, and answers 503 with the indicator's result when the database is unreachable. The controller keeps the repository's conventions: `@ApiTags('Health')` so the routes appear in Swagger, and no version decorator, so they sit beside the home route rather than under `/v1`. Because the global prefix applies, the paths are `/api/health/live` and `/api/health/ready` with the default `API_PREFIX=api`; the pull request says so and shows the one-line change to the `exclude` list in `src/main.ts` if you want them at the root instead.

The e2e tests follow `test/user/auth.e2e-spec.ts`, which uses Supertest against `APP_URL` built from `APP_PORT` and runs under `test/jest-e2e.json` with ts-jest. A new `test/health/health.e2e-spec.ts` expects 200 from the live route, 200 with a `database` entry marked `up` from the ready route, and no authentication on either. The agent runs the suite the way the repository does, through `docker-compose.relational.ci.yaml`, which starts `postgres:17.9-alpine` and maildev beside the API container built from `relational.e2e.Dockerfile`.

Last, the container. `Dockerfile` starts from `node:24.14.1-alpine` and `startup.relational.dev.sh` waits for Postgres with `wait-for-it.sh`, runs migrations and seeds, then `npm run start:prod`. The agent adds a `HEALTHCHECK` that calls the ready route with `wget`, which the Alpine image has, and documents the two paths in the README's environment section next to `APP_PORT` and `API_PREFIX`.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Add health checks to this NestJS API. Read src/main.ts, src/app.module.ts, src/database/config/database.config.ts, src/home/home.controller.ts, test/user/auth.e2e-spec.ts, test/jest-e2e.json, docker-compose.relational.ci.yaml and the Dockerfile first. Add @nestjs/terminus at the version matching @nestjs/common, and a HealthModule with GET /health/live, which answers 200 with no dependency checks, and GET /health/ready, which pings the database through the TypeORM or Mongoose indicator chosen by the same isDocumentDatabase switch app.module.ts uses, with a timeout, and answers 200 or 503 with the indicator result. Keep the routes unauthenticated, tag them for Swagger, and say in the pull request what paths they get under the global prefix. Add an e2e spec under test/health/ in the style of the existing specs and run the relational e2e suite through docker compose. Add a HEALTHCHECK on the ready route to the Dockerfile. Open a pull request with the paths, the test output and the one-line change needed to serve the routes at the root instead.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch with `src/health/health.module.ts`, `src/health/health.controller.ts`, the import in `src/app.module.ts`, `test/health/health.e2e-spec.ts`, the `HEALTHCHECK` line in `Dockerfile`, and `package.json` with `package-lock.json` for `@nestjs/terminus`. The description names the final paths under the prefix, which indicator the ready route uses for each database, and the timeout.

The session's transcript holds the dependency install, the compose build and start, the e2e run with its output, and any failure on the way, in order. The repository's own `CLI tests` workflow runs on the pull request as well, so the generator suites that run on every change report alongside.

A reviewer checks three things before merging: that the live route touches nothing that can fail, that the ready route's timeout is shorter than the orchestrator's probe interval, and that the routes are reachable at the path the deployment expects, since the global prefix moves them under `/api` by default. One follow-up message to the same session changes the prefix handling or adds a check for the mail host, and the sandbox runs the suite again.

## Run it on every new service without a health route

A platform team adding probes to several NestJS services starts one run per repository, a `POST /o/{org}/sessions` with that repository in `resources` and this prompt. An `Idempotency-Key` per repository makes a retried request return the run it already started.

Each run has its own sandbox and its own Docker daemon, so the compose-based e2e suites of several services run at the same time without sharing ports. A webhook on the key posts `run.completed` for each turn with the `session_url` to read the pull request from, and a run whose suite does not pass is reported rather than merged.

## Why run it on Rush?

Testing a readiness probe means running the API against a real database, taking the database away, and watching the route turn to 503. The repository's own e2e setup does that through Docker Compose, which on a laptop means a Postgres container, a maildev container and an API container bound to local ports. In a Rush sandbox the agent runs that stack on its own daemon, and the sandbox parks when the turn ends.

The pull request carries the compose run and the e2e output in the transcript, so a reviewer sees the ready route answering 200 with the database up and the spec passing, rather than trusting a description. The same prompt runs against several services at once, each in its own sandbox, each ending in its own pull request.

## Frequently asked questions

### Why two endpoints rather than one /health?

Because they answer different questions. A liveness probe asks whether the process should be restarted and must not depend on the database, or a database outage turns into a restart loop. A readiness probe asks whether traffic should be routed here and may fail while the database is down. Kubernetes and most load balancers expect the two separately.

### Does the ready check open a new database connection?

No. The TypeORM indicator runs a query through the connection TypeORM already holds, and the Mongoose indicator uses the existing connection's state. The timeout the agent sets keeps a slow database from making the probe hang.

### The boilerplate supports Postgres and MongoDB. Which one gets the check?

Both. The health module picks the indicator with the same `isDocumentDatabase` switch that `src/app.module.ts` uses to load TypeORM or Mongoose, inside the same comment markers, so the repository's generator that strips one database still leaves a working check.

### Are the routes behind the JWT guard?

No. Guards in this repository are applied per controller with `@UseGuards`, not globally, so the health controller simply does not add one. The e2e spec asserts both routes answer without a token.

### Will the Docker HEALTHCHECK work on the Alpine image?

Yes. The image is `node:24.14.1-alpine`, which ships `wget`, so the check calls the ready route with it rather than `curl`. The check is in the application `Dockerfile`; the e2e Dockerfiles are for tests and are left alone.
