Contents
1. プロジェクトの作成と依存クレートの追加
1‑1 プロジェクトの雛形を作る
まずは Cargo の新規プロジェクトを生成します。ターミナルで以下を実行してください。
|
1 2 3 |
cargo new axum_tokio_demo --bin cd axum_tokio_demo |
--bin オプションにより、src/main.rs が自動的に作成されます。
1‑2 依存クレートを Cargo.toml に追加する
推奨の書き方
バージョン番号は「最新安定版が利用可能であること」を示すため、キャレット(^) または 範囲指定(>=) を使います。具体的なバージョンは cargo add コマンドで自動取得できます。
|
1 2 3 4 5 6 7 |
# Cargo の拡張コマンドを利用すると依存とバージョンが自動挿入されます cargo add axum@^0.7 --features json cargo add tokio@^1 --features full cargo add serde@^1 --features derive cargo add serde_json@^1 cargo add tower@^0.4 |
手動で書く場合の例(参考)
|
1 2 3 4 5 6 7 8 9 10 11 12 |
[package] name = "axum_tokio_demo" version = "0.1.0" edition = "2021" [dependencies] axum = { version = "^0.7", features = ["json"] } # Json 抽出器を有効化 tokio = { version = "^1", features = ["full"] } # フル機能のランタイム serde = { version = "^1", features = ["derive"] } serde_json= "^1" tower = "^0.4" |
ポイント
-^0.7は「0.7 系列の最新安定版」を意味し、将来 0.8 がリリースされても自動的に上がらない安全策です。
-cargo addは依存追加と同時にCargo.lockを更新してくれるので、手作業でバージョンを追記するよりミスが減ります。
1‑3 データベースクレート(例: sqlx)を導入する
Axum だけでも動きますが、実務では DB 接続が必要になることが多いです。ここでは PostgreSQL 用プール sqlx::PgPool を例にします。
|
1 2 3 |
# sqlx と postgres ドライバ(tokio ランタイム対応)を追加 cargo add sqlx@^0.7 --features "postgres,tokio-native-tls" |
システム依存パッケージのインストール(Linux/macOS)
- Ubuntu/Debian 系
bash
sudo apt-get update && sudo apt-get install -y libpq-dev build-essential pkg-config - Fedora/CentOS 系
bash
sudo dnf install postgresql-devel gcc make pkgconfig - macOS (Homebrew)
bash
brew install postgresql
libpq-dev(PostgreSQL のクライアントヘッダ)は sqlx がコンパイル時に必要です。Windows ユーザーは公式の PostgreSQL インストーラに同梱されている開発ツールをインストールしてください。
2. 最小構成のサーバ実装
2‑1 エントリポイント src/main.rs の全体像
以下のコードは Tokio ランタイム起動 → Axum Router 構築 → HTTP サーバ起動 を最小限にまとめた例です。コメントで各ステップを補足しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
use axum::{ routing::get, Router, }; use std::net::SocketAddr; // Tokio が提供するマクロで非同期 `main` 関数を生成 #[tokio::main] async fn main() { // 1️⃣ ルーティングテーブルの作成 let app = Router::new().route("/", get(root_handler)); // 2️⃣ バインドアドレス(ローカル開発用) let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("🚀 Server listening on {}", addr); // 3️⃣ Axum のサーバを起動し、`await` で完了まで待機 axum::Server::bind(&addr) .serve(app.into_make_service()) .await .expect("server failed"); } // シンプルな GET ハンドラ。文字列は自動的に `IntoResponse` に変換されます。 async fn root_handler() -> &'static str { "Hello, Axum 0.7 + Tokio!" } |
補足
-#[tokio::main]はデフォルトでマルチスレッドランタイムを生成します。テストやベンチマーク時にシングルスレッドが欲しい場合は#[tokio::main(flavor = "current_thread")]と書き換えてください。
-axum::Server::bindの引数は&SocketAddrで、parse()よりも型安全です。
2‑2 コードのビルドと動作確認
|
1 2 3 4 5 6 7 |
cargo run # => コンソールに "🚀 Server listening on 127.0.0.1:3000" が表示されたら成功 # 別ターミナルで curl を叩く(またはブラウザでアクセス) curl http://127.0.0.1:3000/ # => Hello, Axum 0.7 + Tokio! |
3. 非同期ハンドラとルーティングの実例
3‑1 GET ハンドラ:Path パラメータ取得
概要
URL の一部を変数として受け取り、型安全に処理します。Path<T> は serde::Deserialize を実装した型であれば任意の構造体にマッピングできます。
|
1 2 3 4 5 6 7 8 9 10 |
use axum::{ extract::Path, response::IntoResponse, }; /// `/users/:id` に対するハンドラ例 async fn get_user(Path(user_id): Path<u64>) -> impl IntoResponse { format!("User ID: {}", user_id) } |
3‑2 POST ハンドラ:JSON ボディの受け取り
概要
クライアントから送られる JSON データを自動でデシリアライズし、Json<T> 型として取得します。エラーが発生した場合は Axum が 400 Bad Request を返すので、ハンドラ側では Result<_, _> を書く必要はありません。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use axum::{ extract::Json, response::IntoResponse, http::StatusCode, }; use serde::Deserialize; #[derive(Deserialize)] struct CreateTodo { title: String, completed: bool, } async fn create_todo_handler(Json(payload): Json<CreateTodo>) -> impl IntoResponse { // 実際の DB 保存は省略。ここでは 201 Created を返すだけ。 (StatusCode::CREATED, format!("Created: {}", payload.title)) } |
3‑3 Router への登録例
概要
複数エンドポイントをまとめて Router に組み込む方法です。routing::{get, post} をインポートして可読性を高めます。
|
1 2 3 4 5 6 7 8 9 |
use axum::{ routing::{get, post}, Router, }; let app = Router::new() .route("/users/:user_id", get(get_user)) .route("/todos", post(create_todo_handler)); |
ポイント
-Routerはイミュータブルなビルダーなので、途中で.clone()して別のサブアプリとして再利用できます。
4. 標準 Extractor の活用パターン
4‑1 Path パラメータ抽出(詳細)
Path<T> は URL の変数部分だけを取り出すために使います。以下は構造体で複数パラメータを受け取る例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use axum::extract::Path; use serde::Deserialize; #[derive(Deserialize)] struct ArticleInfo { year: u32, month: u8, slug: String, } async fn article_handler(Path(info): Path<ArticleInfo>) -> String { format!("{}-{:02} {}", info.year, info.month, info.slug) } |
4‑2 Query パラメータ抽出
検索条件やページング情報は Query<T> が便利です。Option<T> を組み合わせると省略可能なパラメータを自然に表現できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
use axum::extract::Query; use serde::Deserialize; #[derive(Deserialize)] struct ListParams { page: Option<u32>, per_page: Option<u32>, } async fn list_handler(Query(params): Query<ListParams>) -> String { let page = params.page.unwrap_or(1); let per_page = params.per_page.unwrap_or(20); format!("Listing page {} ({} items)", page, per_page) } |
4‑3 JSON 抽出とバリデーション
serde の属性や外部クレート validator と組み合わせることで、受信した JSON に対するサーバ側の検証ロジックをシンプルに書けます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
use axum::extract::Json; use serde::Deserialize; use validator::Validate; #[derive(Deserialize, Validate)] struct Register { #[validate(email)] email: String, #[validate(length(min = 8))] password: String, } async fn register_handler(Json(payload): Json<Register>) -> impl IntoResponse { if let Err(e) = payload.validate() { // バリデーションエラーは 422 Unprocessable Entity に変換 return (axum::http::StatusCode::UNPROCESSABLE_ENTITY, format!("{:?}", e)).into_response(); } (axum::http::StatusCode::OK, "登録完了".to_string()) } |
備考
validatorクレートはcargo add validator@^0.16 --features deriveで追加できます。
5. 共有状態とカスタム Extractor の実装
5‑1 State 抽出器で安全にリソースを共有する
概要
データベースプールやキャッシュは Arc<T> に包んで State<Arc<T>> としてハンドラへ注入します。これにより スレッドセーフかつロックフリー なアクセスが可能です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
use axum::{ extract::State, Router, }; use std::{sync::Arc, net::SocketAddr}; use sqlx::postgres::PgPool; #[derive(Clone)] struct AppState { db_pool: PgPool, } // ヘルスチェックハンドラの実装例 async fn health_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse { match state.db_pool.acquire().await { Ok(_) => (axum::http::StatusCode::OK, "OK"), Err(_) => (axum::http::StatusCode::SERVICE_UNAVAILABLE, "DB error"), } } |
main での初期化と Router への組み込み
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#[tokio::main] async fn main() { // PostgreSQL 接続文字列は環境変数や .env ファイルから取得すると安全です let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); let db_pool = PgPool::connect(&database_url).await.unwrap(); let shared_state = Arc::new(AppState { db_pool }); // ルーティングに状態を注入 let app = Router::new() .route("/health", axum::routing::get(health_handler)) .with_state(shared_state); // サーバ起動(前述のコードと同様) let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("🚀 listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } |
5‑2 カスタム Extractor:認証トークンの検証
概要
FromRequest トレイトを実装すると、リクエスト受信直後に任意の非同期処理(例: JWT の検証や外部 API 呼び出し)を走らせられます。以下は Bearer Token を取得・検証するシンプルな実装です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
use axum::{ async_trait, extract::{FromRequest, RequestParts}, http::StatusCode, response::{IntoResponse, Response}, }; use std::sync::Arc; /// 認証が成功したときにハンドラへ渡す情報 #[derive(Debug)] pub struct AuthToken { pub user_id: i64, } /// エラー種別(HTTP ステータスコードにマッピング) #[derive(Debug)] pub enum AuthError { MissingHeader, InvalidFormat, InvalidToken, } impl IntoResponse for AuthError { fn into_response(self) -> Response { let (status, msg) = match self { AuthError::MissingHeader => (StatusCode::UNAUTHORIZED, "Authorization header missing"), AuthError::InvalidFormat => (StatusCode::BAD_REQUEST, "Invalid Authorization format"), AuthError::InvalidToken => (StatusCode::FORBIDDEN, "Invalid token"), }; (status, msg).into_response() } } #[async_trait] impl<B> FromRequest<B> for AuthToken where B: Send, { type Rejection = AuthError; async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> { // 1️⃣ Authorization ヘッダー取得 let header_value = req .headers() .get(axum::http::header::AUTHORIZATION) .ok_or(AuthError::MissingHeader)? .to_str() .map_err(|_| AuthError::InvalidFormat)?; // 2️⃣ "Bearer " プレフィックスを除去 let token = header_value .strip_prefix("Bearer ") .ok_or(AuthError::InvalidFormat)?; // 3️⃣ 非同期でトークン検証(ここではモック実装) verify_token(token).await.map_err(|_| AuthError::InvalidToken) } } // ダミーの非同期検証関数。実運用では JWT ライブラリや DB を叩く。 async fn verify_token(token: &str) -> Result<AuthToken, ()> { if token == "secret-token" { Ok(AuthToken { user_id: 42 }) } else { Err(()) } } |
ハンドラ側での利用例
|
1 2 3 4 5 6 7 8 9 |
use axum::routing::get; async fn protected_handler(auth: AuthToken) -> impl IntoResponse { format!("Hello, user {}", auth.user_id) } // Router に組み込むだけで自動的に認証が走ります let app = Router::new().route("/protected", get(protected_handler)); |
注意点
- カスタム Extractor はFromRequestの型引数Bがリクエストボディの型です。ほとんどの場合はaxum::body::Bodyで問題ありませんが、独自のボディタイプを使う場合はトレイト境界を合わせてください。
6. 統一エラーハンドリングとテスト
6‑1 JSON 形式の汎用エラー型
概要
API のクライアントは 同じ構造 のエラーレスポンスを期待します。以下の ApiError は IntoResponse を実装しているので、任意のハンドラから Result<_, ApiError> を返すだけで JSON 形式に自動変換されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
use axum::{ response::{IntoResponse, Response}, Json, }; use http::StatusCode; use serde::Serialize; #[derive(Debug, Serialize)] pub struct ApiError { pub code: u16, pub message: String, } // 任意のエラー型から簡単に変換できるように実装 impl<E> From<E> for ApiError where E: std::fmt::Display, { fn from(err: E) -> Self { ApiError { code: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), message: err.to_string(), } } } impl IntoResponse for ApiError { fn into_response(self) -> Response { let status = StatusCode::from_u16(self.code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); (status, Json(self)).into_response() } } |
ハンドラでの使用例
|
1 2 3 4 5 6 7 |
async fn db_example_handler() -> Result<impl IntoResponse, ApiError> { // 何らかの DB 操作が失敗したケースをシミュレート let simulated_error: Result<(), &str> = Err("DB 接続に失敗しました"); simulated_error.map_err(Into::into)?; Ok((axum::http::StatusCode::OK, "成功")) } |
6‑2 ハンドラ単体テストの書き方
概要
tower::ServiceExt::oneshot を利用すると、実際にサーバを起動せずに Router に対して HTTP リクエストオブジェクトを投げられます。外部依存(DB 等)はモックや MockPool に差し替えてテスト可能です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
use axum::{ body::Body, http::{self, Request}, Router, }; use serde_json::json; use tower::ServiceExt; // `.oneshot()` 用 #[tokio::test] async fn test_create_todo_success() { // テスト対象のハンドラだけを組み込んだミニアプリ let app = Router::new().route( "/todos", axum::routing::post(super::create_todo_handler), ); let payload = json!({ "title": "Write tests", "completed": false }); let request = Request::builder() .method(http::Method::POST) .uri("/todos") .header(http::header::CONTENT_TYPE, "application/json") .body(Body::from(payload.to_string())) .unwrap(); // `oneshot` でリクエストを投げ、レスポンスを取得 let response = app.oneshot(request).await.unwrap(); assert_eq!(response.status(), http::StatusCode::CREATED); } |
DB モックの簡易例(sqlx)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
use sqlx::{Pool, Postgres}; use std::sync::Arc; // テスト時は `PgPool` の代わりに空のダミープールを作成し、必要なメソッドだけ実装します。 #[derive(Clone)] struct MockPool; impl MockPool { async fn acquire(&self) -> Result<(), sqlx::Error> { Ok(()) // 常に成功するモック } } // `AppState` の型定義をテスト用に切り替えるだけで、同じハンドラが動作します。 |
6‑3 ローカル実行手順まとめ
- リポジトリのクローン(例)
bash
git clone https://github.com/yourname/axum_tokio_demo.git
cd axum_tokio_demo - 環境変数設定(PostgreSQL を使う場合)
bash
export DATABASE_URL=postgres://user:pass@localhost/sample_db - アプリ起動
bash
cargo run
# => 127.0.0.1:3000 がリッスン状態になるはずです - エンドポイントの確認(curl)
bash
curl http://127.0.0.1:3000/
# => "Hello, Axum 0.7 + Tokio!" - テスト実行
bash
cargo test --quiet
# 全てのテストがパスすれば成功です
7. まとめ
- 依存クレートは緩やかなバージョン指定(
^0.7,^1)で管理し、cargo addを活用すると将来のアップデートが楽になります。 - Tokio ランタイムと Axum Router の組み合わせだけで最小サーバが構築でき、さらに
State<Arc<T>>による共有リソースやカスタム Extractor で実務的な要件にも対応可能です。 - 標準 Extractor(
Path,Query,Json)はすべて 非同期に動作し、型安全 なので、バリデーションや認証ロジックを外部クレートと組み合わせてもシンプルに記述できます。 - エラーハンドリングは 共通の
ApiError型 に集約し、IntoResponseを実装すればハンドラ側はエラー処理を書かずに済みます。 tokio::testとtower::ServiceExt::oneshotを使えばサーバ起動不要で 高速な単体テスト が可能です。DB など外部依存はモックに差し替えてテストの信頼性を保ちましょう。
以上の手順とコード例をベースにすれば、最新 Axum(0.7 系)上で Tokio の非同期ランタイム を最大限活用した堅牢かつ拡張性の高い Web API が短時間で構築できます。ぜひローカル環境で試し、必要に応じて認証・DB 接続・ミドルウェアを追加して本格的なサービスへと発展させてください。