ys memos

Blog

Rustのaxumとutoipaを扱うメモ


rust

2025/05/07


Rustで実装するAPIを、OpenAPIを介してClient実装に活用したいことがある。

そこで、RustコードからOpenAPIを自動生成するutoipaというクレートがあげられ、便利なので将来使うときにすぐに使えるようにメモを残しておく。

新しいTipsなどがあれば、随時更新していきたい。


$ rustc --version
rustc 1.87.0-nightly (8c392966a 2025-03-01)

$ cargo --version
cargo 1.87.0-nightly (2622e844b 2025-02-28)

$ cat Cargo.toml
[package]
name = "sample-axum-utoipa"
version = "0.1.0"
edition = "2024"

[dependencies]
axum = "0.8.4"
tokio = { version = "1.45.0", features = ["full"] }
utoipa = "5.3.1"
utoipa-axum = "0.2.0"

ここではweb application frameworkとしてaxumを使うためutoipa-axumを用いているが、他のフレームワークを使う場合、axtix-webではutoipa-actix-webで同様のことが、rocketではrocket-extras featureで類似のこと(こっちはRoutingは違うかも)ができると思われる。


utoipaは、Rustを使ってコードファーストのアプローチでOpenAPIドキュメント(以下、簡単のためoapi-specとする)を生成するためのクレートである。関連クレートも一つのリポジトリにまとめられているため、何かを探すときはutoipaを参照するのが良いだろう。

中心となるクレートの utoipaは、基本的な機能やマクロなどを提供している。実際、oapi-specを生成でいうと、こちらだけで行うことも可能である。

utoipa-axumは、axumのためのAPIを提供しており、axumのルータやハンドラに対してoapi-specを生成するためのマクロや機能を提供している。特に、axum::Routerとの連携が特に有用で、コードファーストのアプローチの利点を増やすことが可能。

本記事内においては、 utoipa_axum::router::OpenApiRouterを用いるものとする。この目的として、コードファーストにするからには、Routing設定とoapi-spec設定の両方を一つの場所で管理し、ドリフトを防ぐという意図がある。



以下の記述で、routeを定義する。これにより、router, oapi-specの両方を同条件で生成することができる。

OpenApiRouter::new()のrouterはaxum::serveを直下に書かない限り、型を指定する必要がある。

let (router, mut openapi) = utoipa_axum::router::OpenApiRouter::new()
    .routes(utoipa_axum::routes!(f))
    .split_for_parts();
println!("{}", openapi.to_json());
let listener = tokio::net::TcpListener::bind("localhost:8080").await?;
axum::serve(listener, router).await?;

簡素化した最小限の形が以下のようになる。

operation_idは全体で一意であればよく、命名記法なども特に制約はない。

#[utoipa::path(
            get, path    = "/hello",
            operation_id = "hello_get",
            responses(
                (status = 200, description = "Greet", body = String),
                (status = 500, description = "Internal Server Error", body = String)
            )
        )]
pub async fn hello() -> String {
    String::from("Hello, world!")
}
型定義して、それをbodyとするhandler定義 POST

oapi-specに含ませたい型定義は、以下のようにutoipa::ToSchemaトレイトを実装することで、oapi-specに含めることができる。

bodyの場合は、RequestからJsonにするため、serde::Deserializeトレイトも実装する必要がある。反対に、Responseの場合は、serde:::Serializeトレイトを実装する必要がある。

#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct EchoInput {
    pub message: String,
}

#[utoipa::path(
    post, path = "/echo",
    operation_id = "echo_post",
    request_body = String,
    responses(
        (status = 200, description = "Echo", body = String),
        (status = 400, description = "Bad Request", body = String)
    )
)]
pub async fn echo(axum::extract::Json(body): axum::extract::Json<EchoInput>) -> String {
    body.message
}


こちら結構設定が煩雑で、他にもっと良い方法があるかもしれないが、以下のようにすることで、クエリパラメータを指定することができる。

#[derive(utoipa::IntoParams, serde::Deserialize)]
#[into_params(style = Form, parameter_in = Query)]
pub struct MeParams {
    #[param(style = Form, value_type = String, example = "name")]
    pub name: String,
}
#[utoipa::path(
            get, path    = "/me",
            operation_id = "me_get",
            params(MeParams),
            responses(
                (status = 200, description = "Me", body = String),
                (status = 500, description = "Internal Server Error", body = String)
            )
        )]
pub async fn me(query: axum::extract::Query<MeParams>) -> String {
    query.name.clone()
}

utoipayaml featureによって、YAML形式でoapi-specを出力することができる。

$ cargo add utoipa --features yaml

を実行するか、

Cargo.toml
utoipa = { version="5.3.1", features=["yaml"] }

と書き直し、

let (_, mut openapi) = utoipa_axum::router::OpenApiRouter::new()
    .routes(utoipa_axum::routes!(f))
    .split_for_parts();
println!("{}", openapi.to_yaml());

とすると、YAML形式でoapi-specを出力することができる。


OpenAPI定義に基づいた仕様を使うことの利点の一つとして、Swagger UIなどを用いて、APIを簡単に動作できることが挙げられる。

utoipaを使う場合でもその利点を享受でき、さらに、公式が提供しているクレートがそれを手助けしてくれる。

提供されているクレートおよびインストールコマンドは以下のとおり。

すべてをホスティングする場合は、routerに以下のように .merge()をする。(使いたいもののみを選んで、mergeしても良い)

    let (mut router, openapi) = utoipa_axum::router::OpenApiRouter::new()
      // ここにrouteを記述
      .split_for_parts();

    router = router
        .merge(utoipa_scalar::Scalar::with_url(
            "/docs/scalar",
            openapi.clone(),
        ))
        .merge(
            utoipa_swagger_ui::SwaggerUi::new("/docs/swagger-ui")
                .url("/docs/openapi.yml", openapi.clone()),
        )
        .merge(
            utoipa_rapidoc::RapiDoc::with_openapi("/docs/rapidoc/openapi.yml", openapi.clone())
                .path("/docs/rapidoc"),
        )
        .merge(utoipa_redoc::Redoc::with_url("/docs/redoc", openapi));

    let listener = tokio::net::TcpListener::bind("localhost:8080").await?;
    axum::serve(listener, router).await?;

コードファーストのOpenAPI定義のなかでも、utoipaは特にコードとの距離が近く、一元的にroutesを定義できるため、実装したけど漏れているなどの人為ミスを減らしやすい印象。さらに、コードとAPI定義の距離も近いため、レビュワーにとって優しいかもしれない。

ただし、OpenAPI定義とhandlerの実装は手書きのため、この部分でのドリフトは発生してしまうかもしれない。


関連タグを探す