# Generate a Typed API Client for a NestJS Service

Yes. An agent starts the customer's NestJS service, reads its Swagger document, generates a typed Python client and tests it against the live server.

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

## What the agent does

The agent starts by getting the service to boot, which for an older NestJS app takes some reading. In a project like lujakob/nestjs-realworld-example-app, `package.json` has `@nestjs/core` `^7.0.5`, `@nestjs/swagger` `^4.4.0`, `@nestjs/typeorm` `^7.0.0` with `typeorm` `^0.2.24`, the `mysql` driver, `typescript` `^3.8.3` and Jest 25. Two config files exist only as examples: `src/config.ts.example`, which exports the JWT `SECRET`, and `ormconfig.json.example`, which points TypeORM at MySQL on `localhost:3306`, database `nestjsrealworld`, with `synchronize: true`. The agent copies both inside the sandbox, starts MySQL with that database, and runs `npm start`, which is `node index.js`: `require('ts-node/register')` and then `src/main.ts`. The repository has both `package-lock.json` and `yarn.lock`; the agent installs from one, says which, and notes that `argon2` `^0.26.2` is a native module that may need an older Node release than the sandbox default to compile.

`src/main.ts` sets `app.setGlobalPrefix('api')`, builds a `DocumentBuilder` with `.setBasePath('api')` and `.addBearerAuth()`, and calls `SwaggerModule.setup('/docs', app, document)`, so the JSON document is served at `/docs-json`. The agent fetches it from the running server and checks that its paths carry the `/api` prefix the routes actually answer on, since the prefix is set twice. Then it reads the document for what it can and cannot type. The controllers carry `@ApiTags` and, in `article.controller.ts`, `@ApiOperation` and `@ApiResponse`, but the responses name a status and description with no `type`, and the DTOs such as `CreateUserDto` use `class-validator` decorators only, with no `@ApiProperty`. So request bodies and responses reach the document without schemas, and a generated client cannot type them.

The agent generates the client anyway, with `openapi-python-client` from the fetched document, and is explicit about the gap rather than hiding it. Its report lists every operation and whether its request and response are typed by the document or fall back to an untyped dict, and points to where the real shapes live in the source, such as `UserRO` and `UserData` in `src/user/user.interface.ts`, so the customer knows which `@ApiProperty` and `@ApiResponse({ type })` additions would close it. It does not edit the service.

To prove the client works, it writes `smoke.py`, which registers a user through `POST /api/users` so there is data to read, then calls three read endpoints that need no token through the generated client: `GET /api/tags`, `GET /api/articles` and `GET /api/profiles/{username}` for the user it created. It runs the script against the server in the sandbox, type-checks the package with `mypy`, and saves both outputs.

## Start it with one API call

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

```json
{
  "agent": "codex",
  "input": "Start this NestJS service in the sandbox: copy src/config.ts.example to src/config.ts and ormconfig.json.example to ormconfig.json, start the MySQL database ormconfig.json names, install dependencies, and run npm start. Fetch the Swagger JSON from the running server at the path SwaggerModule.setup serves and save it as openapi.json. Generate a typed Python client for every endpoint with openapi-python-client. Write coverage.md listing each operation and whether its request and response are typed by the document, and for untyped ones the source file that defines the real shape. Write smoke.py that registers a user, then calls three read endpoints through the generated client against the running server; save its output as smoke-output.txt. Run mypy on the client. Return client.zip, openapi.json, coverage.md, smoke.py and smoke-output.txt as files. Do not change or push the repository.",
  "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

Five files come back as artifacts on the session, each with a `url` that downloads it without a key for an hour: `client.zip` with the generated Python package (an `httpx`-based client and one module per operation, grouped by tag), `openapi.json` as the server served it, `coverage.md`, `smoke.py`, and `smoke-output.txt` with the three responses. The session's `result`, also in the `run.completed` webhook's `data.result`, summarizes it: how many operations are fully typed, whether `mypy` passed, and whether the smoke run reached all three endpoints.

`coverage.md` is the file to read first. For a service whose DTOs have no Swagger decorators, most operations show an untyped body, and the file says which source type each should use. Before shipping the client to the customer, check that `smoke-output.txt` shows real responses from `/api/tags`, `/api/articles` and the profile, and decide whether to deliver the client as it is or ask the customer to add the decorators and regenerate.

## Run it on every change to the customer's controllers

Your backend can start a generation whenever a customer pushes a commit that touches `*.controller.ts` or `*.dto.ts`. Put the repository at that commit's branch in `resources`, use the commit SHA in the `Idempotency-Key`, and store the returned `execution_id` with the customer and commit.

When `run.completed` reaches the key's webhook, verify the signature, download `client.zip` and `coverage.md` from the session's artifacts, and publish the new client version. Comparing this run's `coverage.md` with the last one shows the customer whether their typing coverage went up or down with that change.

## Why run it on Rush?

An older NestJS service is hard to run on demand: a specific TypeScript and ts-node, a native `argon2` build, a MySQL database, and config files that exist only as examples. Each Rush session sets that up in its own sandbox, so a customer's database and Node version never collide with another customer's, and nothing is left running afterwards on your side.

Generating from the live document and calling the live server is what separates a usable client from a plausible one. The smoke run, the `mypy` result and the coverage report are files that came from commands that ran, kept in the session with the full transcript, so you can show a customer exactly how their client was made and what it does not yet cover.

## Frequently asked questions

### Why is my generated client full of untyped dicts?

Because the OpenAPI document has no schemas for those operations. `@nestjs/swagger` only describes a DTO's fields when they carry `@ApiProperty` (or the Swagger CLI plugin is enabled), and a response only gets a schema from `@ApiResponse({ type })`. `coverage.md` lists each gap and the source type that should fill it.

### Can the agent add the missing Swagger decorators?

Yes, as a separate request. This prompt forbids changing the repository; a follow-up to the same session, or a new session, can ask for a pull request that adds `@ApiProperty` to the DTOs and response types to the controllers, after which the client is regenerated with full types.

### Which Python generator does it use?

The prompt names `openapi-python-client`, which writes an `httpx`-based package with typed models. Name another in the prompt, such as `openapi-generator` with its `python` generator, and the agent uses that instead.

### What if the service uses Postgres instead of MySQL?

The agent reads the TypeORM config, or the Prisma schema if the project uses Prisma, and starts the database it names. The prompt says to start whatever database `ormconfig.json` names, so the same request works for either.
