【ITニュース解説】Layered Architecture: Rust API with Axum, SQLx
2026年09月12日に「Dev.to」が公開したITニュース「Layered Architecture: Rust API with Axum, SQLx」について初心者にもわかりやすく解説しています。
ITニュース概要
RustのAxum APIで、大規模開発向けにLayered Architectureを適用する方法を解説。機能を複数の独立したクレートに分割し、APIが直接データベースにアクセスせず、インターフェース経由でやり取りする構造を採用。これにより、各層の役割を明確化し、特にメモリ内ストアで高速なテストを実現し、高い保守性を保つ。
ITニュース解説
このニュース記事は、RustとAxumフレームワークを使ってAPI(アプリケーションプログラミングインターフェース)を開発する際に、大規模なプロジェクトでシステムの保守性を高めるための「レイヤードアーキテクチャ」という設計手法について解説している。多くのRust/Axumのプロジェクトでは、APIのリクエストを処理する部分(ハンドラ)が、データベースへの問い合わせ、ビジネスロジックの実行、結果の返却といった全ての処理を一つのファイルにまとめてしまうことがある。これは簡単なデモプロジェクトでは問題ないが、本番環境で30以上のエンドポイントを持ち、複雑な状態管理、認証、ワーカー間のリソース管理といった多くの機能を実装する場合、コードが複雑になりすぎて管理が困難になるという問題が指摘されている。
記事の筆者は、自身が開発した「IronFlow」というワークフローエンジンプロジェクトで、この問題を解決するために厳格なレイヤードアーキテクチャを採用した経験を紹介している。このアーキテクチャでは、システムを複数の独立した層(レイヤー)に分割し、それぞれの層の役割を明確に定義している。そして、依存関係は常に一方向(上位の層が下位の層を利用することはできるが、下位の層が上位の層を利用することはできない)に限定される。例えば、APIのリクエストを受け付けるハンドラは、直接データベースにアクセスするSQLクエリを実行することはなく、データベースから取得するデータモデル(エンティティ)は、AxumのようなWebフレームワークの存在を一切知らないように設計されている。
具体的なシステム構成として、IronFlowプロジェクトはRustの「Cargoワークスペース」という機能を利用し、システム全体を20個の小さな独立したモジュール(クレート)に分割している。主要なクレートには、以下のような役割がある。
ironflow-store/: データベースとやり取りするためのデータモデル(エンティティ)や、データアクセス方法の定義(traitという共通インターフェース)と、その具体的な実装(PostgreSQLデータベース用とテスト用のメモリ内実装)が含まれる。
ironflow-engine/: ワークフローの実行順序を制御したり、複数の処理を連携させたりするオーケストレーション機能を担当する。
ironflow-api/: Axumフレームワークを使ったAPIのエンドポイント(リクエストを処理するハンドラ)、外部とのデータの送受信に使われる形式(DTO: Data Transfer Object)、共通処理を行うミドルウェア、APIの仕様を記述するOpenAPIドキュメントなどが含まれる。
ironflow-auth/: JWT(認証トークン)を使った認証機能を提供する。
他にも、AI連携機能や、複数のクレートで共通して使われる型定義など、それぞれの専門領域に特化したクレートが存在する。
この分離されたアーキテクチャの重要なポイントは、ironflow-apiクレートがデータベースアクセスライブラリ(SQLx)を直接呼び出せない点にある。代わりに、ironflow-storeクレート内で定義された「Store trait」という共通のインターフェースを通してのみデータにアクセスする。これにより、各層の責任範囲が明確になり、ある層の内部的な変更が他の層に予期せぬ影響を与えるリスクを最小限に抑えることができる。
データモデルの定義を行う「エンティティ層」は、ironflow-store/src/entities/に配置されている。ここでは、システム内で扱われる主要な概念(ワークフローの実行、個々のステップ、それらのステータスなど)が、AxumやSQLxといった特定のフレームワークに依存しない、純粋なデータ構造として定義されている。この層の核となるのは「有限状態機械(FSM)」と呼ばれる、データの状態とその遷移ルールを管理する仕組みだ。例えば、ワークフローの実行ステータスをRunStatusという列挙型(決められた選択肢の中から選ぶ型)で定義し、ある状態から別の状態へ遷移できるかどうかをcan_transition_toというメソッドで厳密にチェックする。これにより、プログラムが意図しない状態遷移を起こすことを防ぎ、システム全体の安定性を高めることができる。Rustのmatches!マクロを使うことで、この状態遷移ルールが非常に読みやすく記述されており、コンパイラが間違いを検知してくれるため、堅牢なシステムを構築するのに役立つ。
次に「Store層」は、データアクセスに関する全ての処理を抽象化する役割を担っている。ここでは、データベースへのデータの保存、取得、更新といった非同期操作を行うための共通インターフェース(trait)が定義されている。そして、このtraitに対する具体的な実装として、本番環境で実際にPostgreSQLデータベースにアクセスする「PostgresStore」と、テストや開発環境でデータベースを必要とせず、データをメモリ上に保持する「InMemoryStore」の2種類が用意されている。例えば、RunStoreというtraitには、新しい実行データを作成するcreate_runや、特定の実行データを取得するget_runなどのメソッドが定義されており、PostgresStoreとInMemoryStoreの両方がこれらのメソッドを実装している。この設計により、APIのハンドラは、どちらの具体的な実装が使われているかを意識することなく、Arc<dyn Store>という共通のインターフェースを通じてデータにアクセスできる。
エラー処理もこのアーキテクチャの重要な側面だ。Store層で発生するエラー(例えば、「指定されたデータが見つからない」「無効な状態遷移が発生した」など)は、あくまでデータ永続化層(ストレージ層)固有のエラーとして定義される。これらのエラーは、直接HTTPステータスコード(例えば「404 Not Found」)に結びつくものではない。HTTPステータスコードへの変換は、API層に到達した段階で初めて行われる。例えば、「実行が見つからない」というストレージエラーがAPI層に渡されたとき、それがHTTPの「404 Not Found」というステータスコードに変換されてクライアントに返される。このようにエラーの発生源と、そのエラーを外部にどのように伝えるかを分離することで、各層が自身の責任範囲に集中し、独立性を保つことができる。
最後に「API層」は、これら全ての要素を組み合わせて、外部に公開するWeb APIを構築する。この層では、Axumフレームワークを使ってHTTPリクエストを受け付け、処理を実行するハンドラを定義する。ここでも重要な設計原則として、Store層で定義された内部的なデータモデル(エンティティ)と、APIを通じて外部に公開するデータ形式(DTO: Data Transfer Object)を厳密に分離している点が挙げられる。例えば、データベースから取得したRunという内部エンティティをそのまま外部に公開するのではなく、RunResponseという専用のDTOを作成し、外部に公開する情報だけを選んでマッピングする。この変換処理は明示的に行われるため、内部モデルの変更が直接APIの公開契約に影響を与えることを防ぎ、APIの安定性を保つ。
APIハンドラの典型的な処理は、まずリクエストの認証情報を検証し、次にStore層から必要なデータを取得し、最後に取得したデータをDTOに変換してクライアントに返却するという流れである。tokio::join!のような並行処理機能を利用して、複数のデータ取得処理を同時に実行することで、APIの応答速度を向上させる工夫もされている。
依存性の注入も、このアーキテクチャにおける重要な要素だ。AppStateという構造体を通じて、Arc<dyn Store>のような共有リソースがハンドラに渡される。これにより、ハンドラは具体的なStoreの実装(例えば、PostgresStoreなのかInMemoryStoreなのか)を知ることなく、抽象的なインターフェースを通じてデータにアクセスできる。これは、システムの柔軟性を高める上で非常に有効な手法だ。
このレイヤードアーキテクチャの最大のメリットは、テストのしやすさと信頼性にある。InMemoryStoreを使用することで、PostgreSQLデータベースやDocker環境といった外部インフラを用意することなく、メモリ上で高速にAPIハンドラのテストを実行できる。これにより、テストの実行時間が大幅に短縮され、開発サイクルが加速する。また、各層が明確に分離されているため、特定の機能を変更する際に、他の層に予期せぬ影響を与えにくいという、コードの保守性向上も期待できる。
筆者は、もし次に同様のプロジェクトを立ち上げるなら、「サービス層」という新たな層を導入し、ビジネスロジックをハンドラやエンジンからさらに分離することで、コードのテスト性と管理性を一層高めたいと考えている。
結論として、レイヤードアーキテクチャはソフトウェア開発における古典的な設計パターンだが、Rustの強力な型システムとCargoワークスペースの機能を使うことで、各層の分離をコンパイラが強制してくれるため、開発者のうっかりミスによる違反を防ぎ、より堅牢なシステムを構築できる。特に、Store traitとその複数の実装(本番用とテスト用)を持つことで、外部インフラに依存しない高速で信頼性の高いテストが可能になる点が、このアーキテクチャの大きな強みである。初期投資として、より多くのクレートを作成したり、データ変換処理を記述したりといった手間はかかるが、並行処理や複雑な状態管理を伴う大規模プロジェクトでは、長期的な保守性や開発効率の向上という点で、その投資は十分に正当化されると筆者は強調している。RustでAPI開発を始める際には、まずデータモデル(エンティティ)の分離から始め、必要に応じてStore traitによるデータアクセスの抽象化、DTOによるAPIと内部モデルの分離、そしてコードベースの規模や責任範囲に応じてクレート分割といったレイヤー化を進めていくのが良いアプローチだ。