rgoussu@goussu: ~/library/rust/protocols
~/library/rust/protocols cat api-documentation.md

API documentation in Rust

# OpenAPI, AsyncAPI, protobuf and GraphQL SDL tooling for Axum, Actix Web and Rocket — utoipa and friends, tonic-build, async-graphql SDL export — where derive-macro code-first dominates and contract-first usually means gRPC.

Conceptsaved 2026-08-09 #rust#protocols#documentation#openapi#asyncapi#codegen#frameworks

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's OpenApiRouter registers 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-axum targets 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 .proto into types in build.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