Axumで始めるRustウェブ開発: 15章で学ぶ実践ガイド
こんにちは、Rustエンジニアの皆さん!今回は、Axumフレームワークを使ったウェブ開発について詳しく解説していきます。Axumは高性能で柔軟性の高いRust製のウェブフレームワークです。それでは、さっそく始めましょう!
1. Axumの基本
Axumは非同期処理に優れ、高速なウェブアプリケーションの開発に適しています。まずは、プロジェクトの設定から始めましょう。
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello, Axum!" }));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
この基本的な例では、ルートパスにGETリクエストが来たときに"Hello, Axum!"を返すシンプルなサーバーを作成しています。
2. ルーティング
Axumのルーティングは直感的で柔軟です。複数のエンドポイントを簡単に設定できます。
let app = Router::new()
.route("/", get(root))
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user));
async fn root() -> &'static str {
"Welcome to our API!"
}
async fn list_users() -> &'static str {
"Here's a list of users"
}
async fn create_user() -> &'static str {
"User created"
}
async fn get_user(Path(id): Path<String>) -> String {
format!("User details for ID: {}", id)
}
この例では、複数のエンドポイントとHTTPメソッドを設定しています。
3. ハンドラ関数
リクエストを処理するハンドラ関数を作成します。非同期関数を使うことで、効率的な処理が可能です。
use axum::{Json, extract::Query};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct UserQuery {
name: Option<String>,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
async fn get_user(Query(params): Query<UserQuery>) -> Json<User> {
let name = params.name.unwrap_or_else(|| "Anonymous".to_string());
Json(User {
id: 1,
name,
})
}
この例では、クエリパラメータを受け取り、JSONレスポンスを返すハンドラ関数を定義しています。
4. ミドルウェア
ミドルウェアを使って、リクエスト処理の前後に共通の処理を挿入できます。
use axum::{
middleware::{self, Next},
response::Response,
};
use hyper::Request;
async fn logging_middleware<B>(req: Request<B>, next: Next<B>) -> Response {
println!("Received request: {} {}", req.method(), req.uri());
let response = next.run(req).await;
println!("Sent response: {}", response.status());
response
}
let app = Router::new()
.route("/", get(root))
.layer(middleware::from_fn(logging_middleware));
この例では、リクエストとレスポンスの情報をログに出力するミドルウェアを実装しています。
5. エラーハンドリング
Axumは優れたエラーハンドリング機能を提供しています。カスタムエラータイプも定義できます。
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
struct AppError(anyhow::Error);
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
}
async fn might_fail() -> Result<&'static str, AppError> {
Err(AppError(anyhow::anyhow!("This is a test error")))
}
この例では、anyhow::Error
をラップしたカスタムエラータイプを定義し、それをレスポンスに変換する方法を実装しています。
6. データベース連携
sqlxなどのクレートを使って、データベースとの連携を実装します。
use sqlx::PgPool;
#[derive(sqlx::FromRow)]
struct User {
id: i32,
name: String,
}
async fn get_user(State(pool): State<PgPool>, Path(id): Path<i32>) -> Result<Json<User>, AppError> {
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(&pool)
.await
.map_err(|e| AppError(e.into()))?;
Ok(Json(user))
}
この例では、PostgreSQLデータベースからユーザー情報を取得する関数を実装しています。
7. 認証と認可
JWTを使った認証システムを実装し、セキュアなAPIを構築します。
use jsonwebtoken::{decode, DecodingKey, Validation};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
async fn auth_middleware<B>(
mut req: Request<B>,
next: Next<B>,
) -> Result<Response, StatusCode> {
let token = req
.headers()
.get("Authorization")
.and_then(|auth_header| auth_header.to_str().ok())
.and_then(|auth_value| {
if auth_value.starts_with("Bearer ") {
Some(auth_value[7..].to_owned())
} else {
None
}
});
if let Some(token) = token {
let claims = decode::<Claims>(
&token,
&DecodingKey::from_secret("secret".as_ref()),
&Validation::default(),
)
.map_err(|_| StatusCode::UNAUTHORIZED)?
.claims;
req.extensions_mut().insert(claims);
Ok(next.run(req).await)
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
この例では、JWTトークンを検証し、有効な場合はリクエストを次のハンドラに渡すミドルウェアを実装しています。
8. テスト
Axumは優れたテスト機能を提供しています。単体テストと統合テストを書いてみましょう。
#[cfg(test)]
mod tests {
use super::*;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use tower::ServiceExt;
#[tokio::test]
async fn test_root() {
let app = app();
let response = app
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"Hello, World!");
}
}
この例では、ルートエンドポイントに対する単体テストを実装しています。
9. 非同期処理
tokioを活用して、効率的な非同期処理を実装します。
use tokio::time::{sleep, Duration};
async fn delayed_response() -> &'static str {
sleep(Duration::from_secs(2)).await;
"This response was delayed by 2 seconds"
}
async fn parallel_tasks() -> String {
let (result1, result2) = tokio::join!(
async { sleep(Duration::from_secs(1)).await; "Task 1 complete" },
async { sleep(Duration::from_secs(2)).await; "Task 2 complete" }
);
format!("{}, {}", result1, result2)
}
この例では、遅延レスポンスと並行タスクの実行を示しています。
10. WebSocket
リアルタイム通信のためのWebSocket機能を実装します。
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
response::IntoResponse,
};
async fn websocket_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
ws.on_upgrade(handle_socket)
}
async fn handle_socket(mut socket: WebSocket) {
while let Some(msg) = socket.recv().await {
if let Ok(msg) = msg {
if let Message::Text(text) = msg {
println!("Client sent: {}", text);
socket
.send(Message::Text(format!("You said: {}", text)))
.await
.unwrap();
}
} else {
println!("Client disconnected");
return;
}
}
}
この例では、簡単なエコーサーバーとしてWebSocketハンドラを実装しています。
11. ファイルアップロード
マルチパートフォームデータを使ったファイルアップロード機能を実装します。
use axum::{
extract::Multipart,
response::IntoResponse,
};
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
async fn upload(mut multipart: Multipart) -> impl IntoResponse {
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
let data = field.bytes().await.unwrap();
let mut file = File::create(format!("/tmp/{}", name)).await.unwrap();
file.write_all(&data).await.unwrap();
}
"File uploaded successfully"
}
この例では、マルチパートフォームデータからファイルを受け取り、サーバー上に保存する処理を実装しています。
12. APIドキュメンテーション
OpenAPIを使って、APIドキュメントを自動生成します。
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
#[derive(OpenApi)]
#[openapi(
paths(
get_user,
create_user,
),
components(
schemas(User)
),
tags(
(name = "users", description = "User management API")
)
)]
struct ApiDoc;
let app = Router::new()
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()));
この例では、utoipa
クレートを使用してOpenAPI仕様を生成し、Swagger UIでAPIドキュメントを表示する設定を行っています。
13. パフォーマンスチューニング
ベンチマークを使って、アプリケーションのパフォーマンスを最適化します。
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn benchmark_handler(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
c.bench_function("get_user", |b| {
b.to_async(&rt).iter(|| async {
black_box(get_user(black_box(Query(UserQuery { name: Some("John".to_string()) }))).await)
})
});
}
criterion_group!(benches, benchmark_handler);
criterion_main!(benches);
この例では、criterion
クレートを使用してハンドラ関数のベンチマークを実行しています。
14. デプロイメント
DockerとKubernetesを使って、アプリケーションをデプロイします。
FROM rust:1.68 as builder
WORKDIR /usr/src/app
COPY . .
RUN cargo build --release
FROM debian:buster-slim
COPY --from=builder /usr/src/app/target/release/my_app /usr/local/bin/my_app
CMD ["my_app"]
このDockerfile
は、Rustアプリケーションをビルドし、軽量なDebian環境で実行するための設定です。
15. セキュリティ対策
CORS設定やレート制限など、セキュリティ対策を実装します。
use tower_http::cors::{Any, CorsLayer};
use tower_http::limit::RequestBodyLimitLayer;
let cors = CorsLayer::new()
.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
.allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]);
let app = Router::new()
.route("/", get(root))
.layer(cors)
.layer(RequestBodyLimitLayer::new(1024 * 1024)); // 1MB limit
この例では、CORSの設定とリクエストボディのサイズ制限を実装しています。
以上で、Axumを使ったRustウェブ開発の基本を一通り学びました。
各章でサンプルコードを交えて解説しましたが、いかがでしたでしょうか?
Axumの魅力を存分に感じていただけたと思います。これからのウェブ開発に、ぜひAxumを活用してみてください!
実際のプロジェクトでは、これらの要素を組み合わせて、より複雑で堅牢なアプリケーションを構築することができます。
Rustの安全性とAxumの高性能を活かして、素晴らしいウェブアプリケーションを作成してください!