# Change a Rust service API

> Guide: Update Axum handlers, OpenAPI emitters, generated client specs, Orval output, and CI checks for service API changes.

- Repository: macro-inc/macro
- GitHub: https://github.com/macro-inc/macro
- Human docs: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e
- Complete Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/llms-full.txt

## Source Files

- `rust/cloud-storage/document_storage_service/src/openapi.rs`
- `rust/cloud-storage/document_cognition_service/src/openapi.rs`
- `js/app/scripts/generate-api-schema.ts`
- `js/app/scripts/services.ts`
- `js/app/packages/service-clients/orval.config.ts`
- `.github/workflows/web-app-check-main.yml`
- `rust/cloud-storage/AGENTS.md`

---

---
title: "Change a Rust service API"
description: "Guide: Update Axum handlers, OpenAPI emitters, generated client specs, Orval output, and CI checks for service API changes."
---

Rust service API changes flow from Axum handlers and `utoipa` annotations into local `*_openapi` Rust binaries, then through `js/app/scripts/generate-api-schema.ts` into committed `openapi.json` files and Orval-generated TypeScript under `js/app/packages/service-clients`.

## API generation path

```text
Rust handler + request/response models
  └─ #[utoipa::path(...)] + ToSchema
      └─ api/swagger.rs ApiDoc paths(...) and components.schemas(...)
          └─ src/openapi.rs binary prints ApiDoc::openapi().to_pretty_json()
              └─ js/app/scripts/generate-api-schema.ts writes openapi.json
                  └─ orval.config.ts regenerates generated client/schema files
                      └─ biome formats service-clients
                          └─ --check mode fails if git diff or untracked generated files remain
```

The live services also mount Swagger UI at `/docs` and serve the same OpenAPI document at `/api-doc/openapi.json`.

## Service registry

The generator accepts service names from `js/app/scripts/services.ts` and maps them to Rust crate names in `js/app/scripts/generate-api-schema.ts`.

| Service argument | Rust crate | OpenAPI binary | Generated package | Orval project |
| --- | --- | --- | --- | --- |
| `cloud-storage` | `document_storage_service` | `document_storage_service_openapi` | `service-storage` | `storageService` |
| `properties-service` | `properties_service` | `properties_service_openapi` | `service-properties` | `propertiesService` |
| `document-cognition` | `document_cognition_service` | `document_cognition_service_openapi` | `service-cognition` | `cognitionService` |
| `auth-service` | `authentication_service` | `authentication_service_openapi` | `service-auth` | `authService` |
| `notification-service` | `notification_service` | `notification_service_openapi` | `service-notification` | `notificationService` |
| `static-files` | `static_file_service` | `static_file_service_openapi` | `service-static-files` | `staticFileService` |
| `connection-gateway` | `connection_gateway` | `connection_gateway_openapi` | `service-connection` | `connectionGateway` |
| `contacts-service` | `contacts_service` | `contacts_service_openapi` | `service-contacts` | `contactService` |
| `unfurl-service` | `unfurl_service` | `unfurl_service_openapi` | `service-unfurl` | `unfurlService` |
| `email-service` | `email_service` | `email_service_openapi` | `service-email` | `emailService` |
| `search-service` | `search_service` | `search_service_openapi` | `service-search` | `searchService` |
| `scheduled-action` | `scheduled_action` | `scheduled_action_openapi` | `service-scheduled-action` | `scheduledActionService` |

## Change an existing endpoint

<Steps>
  <Step title="Update the Axum handler contract">
    Change the handler, request type, response type, status codes, and router registration in the Rust service. For request and response structs that must appear in OpenAPI, derive `utoipa::ToSchema` and keep `serde` casing attributes aligned with the wire format.

    Example surfaces:
    - Storage handlers live under `rust/cloud-storage/document_storage_service/src/api/**`.
    - Cognition handlers live under `rust/cloud-storage/document_cognition_service/src/api/**`.
    - Shared request/response models may live under service-local `model/**` modules or shared crates such as `model`, `models_*`, `chat`, or `memory`.
  </Step>

  <Step title="Update the `utoipa::path` annotation">
    Keep the OpenAPI operation synchronized with the handler:
    - HTTP method: `get`, `post`, `put`, `patch`, or `delete`
    - `path = "..."` matching the mounted API path
    - `tag = "..."`
    - request body and params, when used
    - `responses((status = ..., body = ...))`

    If a route is mounted under both `/{version}` and the unversioned router, document the unversioned path in `#[utoipa::path]`, matching the existing service pattern.
  </Step>

  <Step title="Register the operation and schemas in `api/swagger.rs`">
    Add new handlers to `paths(...)` and add request/response models to `components(schemas(...))`.

    Storage uses `rust/cloud-storage/document_storage_service/src/api/swagger.rs`.

    Cognition uses `rust/cloud-storage/document_cognition_service/src/api/swagger.rs`.

    Missing `components.schemas` entries commonly produce incomplete generated TypeScript even when the Rust code compiles.
  </Step>

  <Step title="Regenerate API artifacts">
    From the web app workspace, regenerate only the service you changed when possible.

    ```bash
    cd js/app
    bun run gen-api -- cloud-storage
    bun run gen-api -- document-cognition
    ```

    To regenerate every registered service:

    ```bash
    cd js/app
    bun run gen-api
    ```

    The script builds OpenAPI binaries with `SQLX_OFFLINE=true cargo build`, runs each binary, writes `openapi.json`, removes the previous `generated/` directory, runs Orval, and then runs Biome on `packages/service-clients/`.
  </Step>

  <Step title="Commit generated files">
    Commit the service `openapi.json` and all changed files under that service package’s `generated/` directory. Do not hand-edit generated files; fix the Rust schema source or Orval config and regenerate.
  </Step>
</Steps>

## Document Cognition special generation

`document-cognition` has additional generated artifacts beyond Orval output.

When the target service includes `document-cognition`, `generate-api-schema.ts` also builds or runs:
- `document_cognition_service_models`
- `gen_tool_schemas`

It then runs `scripts/generate-dcs-types.ts`, which generates:
- `service-cognition/generated/schemas/model.ts`
- `service-cognition/generated/tools/schemas.ts`
- `service-cognition/generated/tools/types.ts`
- `service-cognition/generated/tools/tool.ts`

<Note>
The models generator can read `MODELS_JSON` from a local file. The main API generation script sets this automatically after running the Rust `document_cognition_service_models` binary.
</Note>

## Add a new service to generation

For a new Rust service client, add all registry points together:

1. Add a Rust OpenAPI binary, usually named `<crate>_openapi`, that prints `ApiDoc::openapi().to_pretty_json()`.
2. Add a `[[bin]]` entry in the service `Cargo.toml`.
3. Add the service entry in `js/app/scripts/services.ts` with `name`, `dev`, `prod`, `local`, `output`, and `orvalKey`.
4. Add the service-to-crate mapping in `js/app/scripts/generate-api-schema.ts`.
5. Add a matching Orval project in `js/app/packages/service-clients/orval.config.ts`.
6. Run targeted generation and commit `openapi.json` plus `generated/`.

## Orval output modes

`js/app/packages/service-clients/orval.config.ts` controls the generated TypeScript shape.

| Orval client mode | Services in this repo | Output pattern |
| --- | --- | --- |
| `fetch` | auth, cognition, connection, contacts, email, scheduled action, search, static files, unfurl | `generated/client.ts` plus schema files |
| `zod` with `mode: "split"` | notification, properties, storage | `generated/zod.ts` plus split schema files |

Storage, properties, and notification generate Zod validators. Most other services generate fetch clients and TypeScript schemas. Search writes schemas under `service-search/generated/models` instead of `generated/schemas`.

## Verification

Run the checks that match the change size.

```bash
cd js/app
bun run gen-api -- <service-name>
bun run gen-api -- --check
bun run --bun --silent tsc --project ./packages/app/tsconfig.json
```

For Rust changes:

```bash
cd rust/cloud-storage
cargo test -p <crate-name>
just check
just clippy
just format
```

If the API change includes SQLx query or schema changes, update the SQLx offline cache from the workspace root:

```bash
cd rust/cloud-storage
just prepare_db
```

## CI behavior

The web app PR workflow has two path filters:
- `should_run` for frontend package, app, lockfile, Biome, and workflow changes.
- `api_changed` for `rust/cloud-storage/**/*.rs`, Rust lock/config files, flake files, API generation scripts, setup actions, and the workflow.

When `api_changed` is true, the Typecheck job:
1. sets up web prerequisites,
2. restores Rust cache for `rust/cloud-storage`,
3. runs `bun run gen-api -- --check` in `js/app`,
4. runs TypeScript checking.

`--check` mode fails when generated service-client files differ from the committed tree or when untracked generated files exist.

## Troubleshooting

### `Generated types are out of sync with Rust API definitions`

Run generation locally and commit the resulting changes:

```bash
cd js/app
bun run gen-api
```

For a smaller diff, target the changed service:

```bash
bun run gen-api -- cloud-storage
```

### `No matching services found`

Use one of the `name` values from `js/app/scripts/services.ts`, such as `cloud-storage`, `document-cognition`, or `search-service`.

### `No crate mapping found, skipping`

The service exists in `services.ts` but is missing from `serviceToCrate` in `generate-api-schema.ts`. Add the crate mapping before regenerating.

### TypeScript type is missing or too broad

Check the Rust OpenAPI source:
- the model derives `ToSchema`,
- the model is included in `components(schemas(...))`,
- the handler is included in `paths(...)`,
- the `#[utoipa::path]` response body names the expected type.

### Orval writes to the wrong package

Keep these values aligned:
- `services.ts` `output`
- `services.ts` `orvalKey`
- `orval.config.ts` project key
- `orval.config.ts` `input.target`
- `orval.config.ts` `output.target` and `output.schemas`

### OpenAPI binary fails in generation

The generator runs binaries from `rust/cloud-storage/target/debug` unless `OPENAPI_BINS_DIR` is set. Each binary has a 120 second timeout and reports captured stderr on non-zero exit or timeout.

To skip the cargo build phase with prebuilt binaries:

```bash
cd js/app
OPENAPI_BINS_DIR=/path/to/bins bun run gen-api -- <service-name>
```

## Related pages

<CardGroup>
  <Card title="Rust cloud-storage development" href="/rust-cloud-storage-development">
    Service testing, SQLx offline cache, and workspace commands.
  </Card>
  <Card title="Generated service clients" href="/generated-service-clients">
    TypeScript client package layout and generated artifact conventions.
  </Card>
</CardGroup>
