All jobs

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

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

The use case this job follows, with its automation and trigger: Hand off from your terminal.

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.