🪄

【Rust】 axum Extractorのすゝめ #ヌーラボブログリレー2025冬

に公開

この記事はヌーラボブログリレー2025 冬 Tech の5日目として投稿しています。

RustのWebフレームワークであるaxumを使っていると、「ハンドラの引数に好きな型を置くだけで値が注入される」という体験に感動します。これは Extractor と呼ばれる仕組みのおかげです。

Extractorは非常に便利で、Middlewareよりも柔軟かつ型安全にリクエストごとのロジックを記述できます。しかし、Dependency Injectionまで含めた実践的なExtractorの使い方について、日本語でまとまった情報が少ないと感じたため、本記事で紹介します。

特に、ジェネリクスを用いた静的ディスパッチによるDIを採用しようとして、axumの State やトレイト境界の扱いに苦戦している方にとって、本記事の内容は解決の糸口となるでしょう。もちろん、動的ディスパッチを使用している場合や、単にハンドラを綺麗に保ちたい場合でも、本記事のテクニックは有用です。

対象読者

  • axumの基本的な使い方(ルーティング、ハンドラ)を理解している
  • Extractorを活用してハンドラをシンプルに保ちたい
  • State<AppState> で巨大な構造体をハンドラに渡すのを避けたい
  • ジェネリクスを使った静的ディスパッチによるDIに関心がある(特に有益!)

1. Extractorとは

axumにおけるExtractorは、HTTPリクエストから必要なデータ(パスパラメータ、クエリ、JSONボディ、ヘッダーなど)を型安全に取り出すための仕組みです。

例えば、以下のハンドラでは PathJson という2つのExtractorが使われています。

async fn update_user(
    Path(user_id): Path<UserId>,
    Json(payload): Json<UpdateUserPayload>,
) {
    // ...
}

axumは関数のシグネチャを見て、リクエストからデータを抽出し、ハンドラに渡してくれます。

2. Extractorは二種類ある

Extractorを自作したり深く理解したりするには、Extractorには2つのトレイトが存在することを知っておく必要があります。

  1. FromRequest
  2. FromRequestParts

FromRequest

FromRequest はリクエスト全体(ヘッダー + ボディ)を消費するExtractorです。
代表例は Json<T>Form<T> です。
リクエストボディは一度しか読めない(ストリームを消費する)ため、ハンドラの引数として指定できる FromRequest 実装は、必ず最後の一つでなければなりません。

FromRequestParts

FromRequestParts はリクエストの「ヘッダー」や「URL」などのメタデータ部分(Parts)のみを使用するExtractorです。ボディを消費しません。
代表例は Path<T>Query<T>HeaderMap などです。
これらはボディを消費しないため、ハンドラの引数にいくつでも並べることができます。

3. Extractorを自作する(基本編)

特定のヘッダーをパースしたり、認証情報を検証したりするロジックは、Custom Extractorとして切り出すとハンドラがスッキリします。

例として、AuthorizationヘッダーからBearerトークンを抽出するExtractorを実装してみましょう。ここでは FromRequestParts を実装します。

use axum::{
    extract::FromRequestParts,
    http::{header::AUTHORIZATION, request::Parts, StatusCode},
};

pub struct BearerToken(pub String);

impl<S> FromRequestParts<S> for BearerToken
where
    S: Send + Sync,
{
    type Rejection = (StatusCode, &'static str);

    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
        let Some(auth_header) = parts
            .headers
            .get(AUTHORIZATION)
            .and_then(|value| value.to_str().ok())
        else {
            return Err((StatusCode::UNAUTHORIZED, "Missing Authorization header"));
        };

        let Some(token) = auth_header.strip_prefix("Bearer ") else {
            return Err((StatusCode::UNAUTHORIZED, "Invalid Authorization header"));
        };

        Ok(BearerToken(token.to_owned()))
    }
}

// ハンドラでの使用
async fn handler(BearerToken(token): BearerToken) {
    println!("Token: {}", token);
}

このように実装すると、どのハンドラでも BearerToken を引数に取るだけでトークン文字列を取得できるようになります。

4. StateとFromRefを活用したDI戦略

axumには State という機能があり、DB接続プールやServiceなどの依存先をハンドラに注入できます。実はこの State<T> もExtractorの一種です。

Stateの基本

#[derive(Clone)]
struct AppState {
    user_service: Arc<dyn UserService>,
}

// ルーターにStateを登録
let app = Router::new()
    .route("/", get(handler))
    .with_state(AppState { ... });

// ハンドラでStateを受け取る
async fn handler(State(state): State<AppState>) {
    // ...
}

部分抽出によるState肥大化の解消

アプリケーションが大きくなると、AppState が肥大化しがちです。すべてのハンドラで巨大な AppState 全体を受け取ると、ハンドラ内で使用しないフィールドまで Clone されることになり非効率です。

そこで FromRef トレイトを使うと、Stateの一部(サブステート)だけ を抽出できます。

フィールドが具体的な型(Arc<dyn Trait>含む)であれば、#[derive(FromRef)] を使うのが最も簡単です。

use axum::extract::FromRef;

#[derive(Clone, FromRef)]
struct AppState {
    user_service: Arc<dyn UserService>,
    config: AppConfig,
}

// ハンドラは AppState 全体ではなく、必要な UserService だけを要求できる
async fn handler(State(user_service): State<Arc<dyn UserService>>) {
    // ...
}

ジェネリクスを用いた静的ディスパッチ環境でのDI

しかし、ジェネリクスを用いた静的ディスパッチでDIを行っている場合、話はそう単純ではありません。

dyn compatibleではない実装を避け、動的ディスパッチを利用する方が無難ではないか、と思われるかもしれません。
しかし、メソッドに型パラメータを使用できるというのは設計上大変便利なものです。この表現力を維持したままDIを行いたい場合、ここでこそ FromRef の真価が発揮されます。

もし FromRef を使わず、ハンドラで AppState を直接受け取るとどうなるでしょうか。

#[derive(Clone)]
struct AppState<US, AS, NS> {
    user_service: US,
    auth_service: AS,
    notification_service: NS,
}

// ハンドラが全ての型パラメータを知る必要がある!
async fn create_user<US, AS, NS>(
    State(state): State<AppState<US, AS, NS>>,
)
where
    US: UserService,
    AS: Clone,
    NS: Clone,
{
    // state.user_service しか使わないのに、ASやNSの型定義も必要...
}

これには重大な問題があります。

  1. 記述が大変: ハンドラが必要としない AuthServiceNotificationService の型パラメータまで全て記述しなければなりません。
  2. 変更に弱い: AppState に新しいコンポーネント(新しい型パラメータ)が増えると、すべてのハンドラのシグネチャを書き換える必要が生じます。

必要なコンポーネントだけを extract することができれば、ハンドラは「自分が使う型」だけを知っていれば良くなり、保守性が劇的に向上します。

実装の壁:Orphan Rule (Coherence)

しかし、これを実現しようとすると、技術的な壁にぶつかります。
derive マクロがうまく動かないだけでなく、手動実装時にも Orphan Rule (Coherence) の制約を受けるのです。

struct AppState<US, AS, NS> {
    user_service: US,
    auth_service: AS,
    notification_service: NS,
}

ここで、以下のように US に対して直接 FromRef を実装しようとすると、コンパイルエラーになります。

// コンパイルエラー: Orphan Rule違反
// US(別のクレートの型かもしれない)に対して FromRef(別のクレートのトレイト)を実装することはできない
impl<US, AS, NS> FromRef<AppState<US, AS, NS>> for US
where
    US: Clone,
{
    fn from_ref(state: &AppState<US, AS, NS>) -> Self {
        state.user_service.clone()
    }
}

この問題を回避するには、抽出用のラッパー構造体(Newtype) を用意します。そう、いつもお世話になるNewtypeパターンです。
Newtypeパターンについては、私が以前書いた型で守るRustのバリデーション:シンプルなNewTypeから正規化、高度な合成パターンまで でも解説しています。

// 抽出用のラッパー構造体
#[derive(Clone)]
pub struct UserServiceExtractor<S>(pub S);

// ラッパー構造体に対して FromRef を実装する(これは自作型なのでOK)
impl<US, AS, NS> FromRef<AppState<US, AS, NS>> for UserServiceExtractor<US>
where
    US: Clone,
{
    fn from_ref(state: &AppState<US, AS, NS>) -> Self {
        Self(state.user_service.clone())
    }
}

ハンドラ側では、このラッパー経由でサービスを受け取ります。

// ハンドラは他の型パラメータ(AS, NS)を知らなくて良い!
async fn handler<US>(
    State(UserServiceExtractor(service)): State<UserServiceExtractor<US>>,
) {
}

このように UserServiceExtractor を挟むことで、Orphan Ruleを回避しつつ、静的ディスパッチによる柔軟なDIを実現できます。

5. ExtractorからStateを利用する(応用編)

Custom Extractorの中でStateに保持されている依存コンポーネントを利用したい場合、from_request_partsstate 引数を利用します。

ここで有用なのは、「Extractorが必要とする設定を FromRef で要求する」 という手法です。
JWT認証を行う Claims Extractorを例にします。

#[derive(Clone)]
pub struct JwtConfig(Arc<JwtConfigInner>);
struct JwtConfigInner {
    decoding_key: DecodingKey,
}

#[derive(Deserialize, Clone)]
struct Claims {
    sub: String,
}

// S (State) はなんでも良いが、JwtConfig を取り出せる (FromRef) ものでなければならない
impl<S> FromRequestParts<S> for Claims
where
    S: Send + Sync,
    JwtConfig: FromRef<S>,
{
    type Rejection = StatusCode;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        // Stateからconfigを取り出す
        let JwtConfig(conf) = JwtConfig::from_ref(state);

        // ヘッダーからトークンを取り出し、conf.decoding_keyを使って検証...
        // ...

        Ok(Claims { sub: "user123".to_string() })
    }
}

ここで where 句の JwtConfig: FromRef<S> に注目してください。
トレイト境界は S: Trait のように型パラメータそのものに対して記述するイメージが強いかもしれませんが、実は「具体的な型(JwtConfig)が、型パラメータ(S)を使ったトレイトを実装している」という形でも記述できます。

こうすることで、Claims Extractorは特定の AppState 型に依存せず、JwtConfig を持っているStateであればどんなアプリケーションでも再利用可能になります。

なお、ここでは Claims をそのままExtractorとして流用しましたが、実用上はより使いやすくしたExtractorを用意するのも良いでしょう。例えば、以下のような Authorized 型です。

pub struct Authorized(pub UserId);

こうしておけば、ハンドラは Authorized(user_id) を受け取るだけで済み、JWTの中身を意識する必要がなくなります。シグネチャを見るだけで「このAPIは認証済みユーザー専用である」と型レベルでわかります。

補足:Extensionsを利用したキャッシュ

FromRequestPartsparts 引数には extensions というTypeMapが含まれています。これを利用して、Extractor間やMiddleware間でデータの受け渡しやリクエストスコープでのキャッシュが可能です。

これを手軽に実現するための Cached Extractoraxum-extra クレートで提供されています。
Cached<T> は、ラップしたExtractor T の実行結果を自動的に extensions にキャッシュし、次回以降はそこから値を返します。

これが特に役立つのは、「あるExtractorが別のExtractorに依存している」 ケースです。

例えば、「CurrentUser Extractor」は「Session Extractor」に依存しているとします。このとき、ハンドラでも Session を使いたい場合、単純に実装すると Session のロード処理が2回走ってしまいます。

これを防ぐには、CurrentUser の実装内部で Cached<Session> を利用します。

use axum_extra::extract::Cached;

// 1. コストの高い処理(Sessionのロード)を行うExtractor
struct Session { /* ... */ }

// 2. Sessionに依存するExtractor
struct CurrentUser { /* ... */ }

impl<S> FromRequestParts<S> for CurrentUser
where
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        // 直接 Session::from_request_parts を呼ぶのではなく、
        // Cached を経由して呼び出す。
        // これにより、Sessionがロードされ、かつextensionsにキャッシュされる。
        let Cached(session) = Cached::<Session>::from_request_parts(parts, state)
            .await
            .map_err(|err| err.into_response())?;

        // ロードされたsessionを使ってユーザーを特定...
        // ...
        Ok(CurrentUser { /* ... */ })
    }
}

// 3. ハンドラでの利用
async fn handler(
    // 内部で Cached<Session> を呼ぶため、Sessionがロード&キャッシュされる
    current_user: CurrentUser,
    // ここでは既にキャッシュが存在するため、ロード処理なしでSessionを取得できる
    Cached(session): Cached<Session>,
) {
    // ...
}

このように、依存関係のあるExtractor同士で Cached を介在させることで、無駄な再計算を防ぐことができます。

まとめ

本記事では、axumの便利な機能であるExtractorについて、基礎から応用的なDIパターンまで解説しました。

  • Extractorの基礎: FromRequestFromRequestParts の違いを理解し、適切に使い分けることが重要です。
  • StateとDI: State もExtractorの一種であり、FromRef を活用することで巨大な AppState から必要な依存だけをハンドラに渡すことができます。
  • 静的ディスパッチと疎結合: ジェネリクスを用いた静的ディスパッチ環境下でも、Newtypeパターンと FromRef を組み合わせることで、ハンドラがアプリケーション全体の型定義に依存しない、疎結合な設計を実現できます。
  • Custom Extractor: 認証や共通ロジックなどを宣言的に記述できます。

axumのExtractorを使いこなすことは、単にコードを綺麗にするだけでなく、変更に強くテストしやすいアプリケーション設計への近道です。ぜひ、プロジェクトの特性に合わせて活用してみてください。

Discussion