All jobs

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 RushIn 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 with this body:

{
  "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.

The use case this job follows, with its automation and trigger: Run an agent per customer task.

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.