# Add OpenAPI Docs to an Actix Web API

Yes. An agent annotates your Actix Web handlers and models with utoipa, serves openapi.json and Swagger UI, tests the document, and opens a pull request.

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

## What the agent does

The agent clones the API into its own cloud sandbox and reads the routes before it documents them. In a service like the `databases/postgres` crate of actix/examples, `src/main.rs` builds the whole app in one place: an `App` with a `web::ThinData(pool.clone())` for the `deadpool_postgres::Pool`, and one `web::resource("/users")` with `web::post().to(add_user)` and `web::get().to(get_users)`. `add_user` takes `web::Json<User>` and answers `HttpResponse::Ok().json(new_user)`; `get_users` answers a `Vec<User>`. `src/models.rs` defines `User` with `email`, `first_name`, `last_name` and `username`, deriving `Deserialize`, `Serialize` and `PostgresMapper` for the `users` table. `src/errors.rs` maps `MyError::NotFound` to `404`, a `PoolError` to `500` with the error text as the body, and everything else to an empty `500`, and `README.md` notes that posting the same `username` twice hits the unique constraint and answers `500`. The crate's `Cargo.toml` takes `actix-web` 4.11 from the workspace, with `confik`, `deadpool-postgres`, `tokio-postgres`, `tokio-pg-mapper` and `derive_more`; the workspace is edition 2024 with `rust-version` 1.98. There are no tests and no OpenAPI output.

The agent adds `utoipa` with its `actix_extras` feature and `utoipa-swagger-ui` with the `actix-web` feature to `Cargo.toml`. `User` gets `#[derive(ToSchema)]` with a `#[schema(example = ...)]` taken from the README's `ferris@thecrab.com` request. Each handler gets a `#[utoipa::path]` attribute: `get_users` as `get` on `/users` returning `200` with `Vec<User>`, and `add_user` as `post` on `/users` with a `User` request body, `200` with `User`, and `500` described as the pool, mapper and unique-constraint failures `errors.rs` collapses together, since the document should say what the code does rather than what it ought to do. An `ApiDoc` struct with `#[derive(OpenApi)]` lists both paths and the `User` component, and `main.rs` mounts `SwaggerUi::new("/swagger-ui/{_:.*}")` with the document at `/api-docs/openapi.json`. The agent also writes the `MyError::NotFound` response into the document only where a handler can return it, which today is neither, and says so in the pull request.

The test needs no database. `deadpool_postgres` creates its pool lazily, so `actix_web::test::init_service` can build the same `App` from a `Config` with placeholder credentials and never open a connection as long as the test does not call a users handler. The agent adds `src/main.rs` tests, or a `tests/openapi.rs`, that request `/api-docs/openapi.json`, parse it as JSON, and assert that `paths` has `/users` with `get` and `post`, that `components.schemas.User` lists the four fields, and that `/swagger-ui/` answers with HTML. Checks then follow the workspace: `cargo check --workspace --bins --examples --tests` as `.github/workflows/ci.yml` runs it, `cargo clippy -p db-postgres --all-targets --all-features` from the `justfile`, `cargo +nightly fmt --check`, because `.rustfmt.toml` sets `imports_granularity = "Crate"` and `group_imports = "StdExternalCrate"`, which stable rustfmt ignores, and `cargo test -p db-postgres`.

Finally it runs the server for real. Following `README.md`, it starts Postgres in the sandbox, creates `test_user` and `testing_db`, loads `sql/schema.sql`, grants the privileges, writes the `.env` with `SERVER_ADDR`, `PG__USER`, `PG__PASSWORD`, `PG__HOST`, `PG__PORT`, `PG__DBNAME` and `PG__POOL__MAX_SIZE`, runs `cargo run`, fetches `/api-docs/openapi.json`, opens `/swagger-ui/` in the sandbox browser, and executes the `POST /users` example from the UI once to confirm the request body the document describes is the one the handler accepts.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Add OpenAPI documentation to this Actix Web API. Read Cargo.toml, src/main.rs, src/models.rs, src/errors.rs, src/db.rs, sql/schema.sql, README.md, the workspace Cargo.toml, .rustfmt.toml, justfile and .github/workflows/ci.yml. Add utoipa with the actix_extras feature and utoipa-swagger-ui with the actix-web feature, derive ToSchema on the models with examples from the README, annotate each handler with utoipa::path giving its method, path, request body and the responses the code actually returns including the 500 cases in errors.rs, build an ApiDoc with OpenApi, and serve the document at /api-docs/openapi.json with Swagger UI at /swagger-ui/. Write a test with actix_web::test::init_service that fetches the document without a database connection and asserts the paths, methods and schema fields. Run cargo check as ci.yml does, cargo clippy for the crate, cargo +nightly fmt --check and cargo test, then start Postgres as README.md describes, run the server, fetch the document and open Swagger UI in the browser. Open a pull request with the document's path list and the command output in its description.",
  "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 that changes `Cargo.toml` and `Cargo.lock`, `src/main.rs` and `src/models.rs`, and adds the test. The description lists each documented operation with its path, method and responses, shows the `components.schemas.User` block from the generated document, and quotes the tail of the `cargo test`, `clippy` and `fmt` runs. A screenshot of Swagger UI from the sandbox browser is attached as an artifact.

The session read names the repository and the pull request, and the transcript at `GET /o/{org}/sessions/{id}/messages` keeps every command: the dependency resolution, each cargo invocation with its full output, the Postgres setup from the README, and the request the agent sent through Swagger UI. `GET /o/{org}/p/{project}/sessions/{id}/changes` shows each diff.

Before merging, a reviewer reads the `500` descriptions against `errors.rs` to confirm the document is honest about what the handlers return today, checks that `utoipa-swagger-ui` builds in CI without a network fetch at build time, and decides whether Swagger UI should be behind a feature flag in production. A follow-up on the same session can split the `500` into a `409` for the unique-constraint case once the error type distinguishes it.

## Run it on every new JSON API without a document

Run it once from the terminal and keep it for the next service. A script on your server lists the crates in the workspace whose `Cargo.toml` depends on `actix-web` and not `utoipa`, sends one `POST /o/{org}/sessions` per crate with this body and the repository in `resources`, naming the crate directory in `input`, and uses the crate name as the `request_id` so a rerun returns the run already started. Each run compiles in its own sandbox with its own target directory.

Give the key a webhook URL and Rush POSTs `run.completed` with `data.session_url` when each turn ends. A handler reads the session and posts the pull request link and the operation list where the API's owners will see them; a run that ended `needs_review` because `cargo +nightly fmt --check` or the document test failed is listed for a person to read.

## Why run it on Rush?

An OpenAPI document written by hand drifts from the handlers the day after it is merged, and one generated from annotations is only right if the annotations describe what the code returns, including the collapsed `500` in `errors.rs`. The agent reads the error mapping, documents it as it is, and tests the generated document in the same crate, so the pull request carries a document that the test suite will keep honest.

You start it from a terminal and come back to a pull request. The compile happens in the sandbox, not on your machine, and the sandbox parks with its target directory when the turn ends, so splitting an error code or adding a second resource is a follow-up on the same session without a cold build.

## Frequently asked questions

### Does the document test need Postgres?

No. `deadpool_postgres` creates its pool without connecting, so a test that builds the `App` with placeholder config and requests only `/api-docs/openapi.json` never touches a database. The agent still runs the server against a real Postgres afterwards, following the README, to exercise the documented `POST /users` once.

### What does it say about the 500 responses?

What `errors.rs` does: a pool error answers `500` with the error text, and a mapper error or a database error such as the unique `username` constraint answers an empty `500`. The document describes that rather than a `409` the code does not send, and the pull request points at the error type as the place to change it.

### Why does formatting need nightly?

The workspace `.rustfmt.toml` sets `imports_granularity` and `group_imports`, options that stable rustfmt skips with a warning, and the `justfile` runs `cargo +nightly fmt`. The agent installs the nightly toolchain for the formatter only and runs `cargo +nightly fmt --check` so the pull request matches the repository's style.

### Can Swagger UI be left out of production builds?

Yes. The agent mounts it unconditionally so the pull request is small, and notes in the description that the `SwaggerUi` service can sit behind a Cargo feature or an environment check while `/api-docs/openapi.json` stays. Doing that is a follow-up on the same session.
