Overview
Rust has no runtime reflection, so every documentation flavor resolves to compile-time
codegen — the question from the
central note becomes which way the macros point.
Code-first dominates OpenAPI: utoipa and its per-framework kin derive the spec from the
same types serde already owns, so the doc rides on the handler signatures. Contract-first
OpenAPI is the ecosystem's thin spot — thin enough that teams who want a reviewed contract
usually reach for tonic and a .proto instead. This note maps
each flavor onto Axum, Actix Web and Rocket.
Key points
- OpenAPI code-first is utoipa's game:
#[derive(ToSchema)]on the serde types,#[utoipa::path]on handlers, assembled into a spec and served through utoipa-swagger-ui, utoipa-redoc or utoipa-scalar — integrations exist for all three frameworks, and utoipa-axum'sOpenApiRouterregisters route and doc in one call so they cannot diverge. - Per-framework natives: aide derives specs from Axum extractors with less annotation; apistos (paperclip's successor) plays the utoipa role natively on Actix Web; rocket_okapi + schemars does it for Rocket; poem-openapi bakes the whole idea into a framework — the strongest correct-by-construction option, if you accept poem.
- OpenAPI contract-first is thin: progenitor generates reqwest clients from a spec
(Oxide's tooling), and OpenAPI Generator's
rust/rust-axumtargets exist with community-grade maturity — there is no oapi-codegen-grade server standard. - AsyncAPI is manual: no mature Rust codegen. Hand-author the document, lint and render it with the AsyncAPI CLI in CI, and let schemars-produced JSON Schema (or the schema registry) carry the payload contracts for the messaging crates.
- gRPC is contract-first by construction: tonic-build/prost compile
.protointo types inbuild.rs(or via buf); docs render from the proto (protoc-gen-doc, Buf Schema Registry), tonic-reflection gives runtime discovery for grpcurl and friends. - GraphQL is code-first: async-graphql and juniper derive the schema from Rust types;
the SDL is an output — export it (
schema.sdl()) in a test or build step so the doc artifact is versioned, diffable and usable for federation; GraphiQL ships built in. - Drift is a compile error: the schema derives from the same structs serde
(de)serializes, so type-level drift can't happen — what still needs CI (Spectral,
oasdiff,
buf breaking, graphql-inspector) is the semantic surface: descriptions, status codes, deprecations, breaking-change gates.
Details
Flavor × framework matrix
| Flavor | Axum | Actix Web | Rocket |
|---|---|---|---|
| OpenAPI, code-first | utoipa + utoipa-axum (OpenApiRouter), or aide for extractor-derived specs; serve via utoipa-swagger-ui/redoc/scalar |
utoipa (actix feature) or apistos; same UI crates | rocket_okapi + schemars, Swagger UI/RapiDoc bundled |
| OpenAPI, contract-first | OpenAPI Generator rust-axum (community maturity); progenitor for the client side |
OpenAPI Generator rust client; no maintained server target |
Same client-side story; no server target |
| AsyncAPI | Framework-independent: hand-authored spec + AsyncAPI CLI; payloads via schemars/registry — attaches to the messaging consumers, not the router | ← same | ← same |
| gRPC docs | tonic shares the tower stack — REST and gRPC behind one middleware pile, one .proto contract |
Separate tonic server alongside (no tower sharing) | Same — run tonic beside Rocket |
| GraphQL | async-graphql-axum layer; SDL exported from the schema object | async-graphql-actix-web layer | async-graphql-rocket layer |
The gRPC row is the tower-hyper correspondence paying out: because Axum and tonic are both tower services, a contract-first gRPC API and a code-first REST API can share one middleware stack and one binary — the neighbour ecosystems need a gateway for that.
Choosing a direction in Rust
Default to types-first code-first: utoipa (or aide/apistos/rocket_okapi per framework) on
the serde types you were writing anyway, spec exported and gated in CI. Go contract-first
when the contract outranks the implementation — public APIs with polyglot consumers — and
accept that in Rust that usually means either generating clients from your published
spec (progenitor) while the server stays code-first, or moving the boundary to
tonic and a .proto where contract-first is the native
workflow, not the exception.
Related
- Backend protocols in Rust — the map — parent map; the protocols these documents describe.
- API documentation — the flavors and the code-first vs contract-first trade in the abstract.
- REST & HTTP APIs in Rust — utoipa in its REST context, validation and error shapes included.
- Messaging in Rust — Kafka, AMQP & NATS — the channels an AsyncAPI document would describe.
- gRPC in Rust — tonic & prost — the contract-first flavor in full.
- GraphQL in Rust — async-graphql's code-first schemas in detail.
- Rust web frameworks — the landscape — the frameworks this note maps onto.
- API documentation in Java and in Go — the neighbours: annotation-shaped and codegen-shaped where Rust is derive-shaped.
- Hexagonal architecture reference implementation — Rust
— where these contract artifacts live in the house layout: the
*-apicrates.