20
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Rust で Web サーバーを作るなら、axum が最高です。

  • tokio チームが開発
  • tower エコシステムと統合
  • 型安全でエルゴノミック
  • マクロなしでルーティング

この記事では axum の魅力を紹介します。

目次

  1. axum とは
  2. Hello World
  3. ルーティング
  4. リクエストの処理
  5. レスポンスの返却
  6. ミドルウェア
  7. 実践:REST API
  8. まとめ

axum とは

tokio チームが開発した Web フレームワーク。

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }

特徴:

  • マクロ不要 - 普通の関数がハンドラになる
  • 型安全 - コンパイル時にエラーを検出
  • tower 互換 - 豊富なミドルウェア
  • 高速 - tokio ベースの非同期処理

Hello World

use axum::{Router, routing::get};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    
    println!("Server running on http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}
cargo run
curl http://localhost:3000
# Hello, World!

これだけ! シンプル。

ルーティング

基本

use axum::{Router, routing::{get, post, put, delete}};

async fn get_users() -> &'static str { "GET /users" }
async fn create_user() -> &'static str { "POST /users" }
async fn get_user() -> &'static str { "GET /users/:id" }
async fn update_user() -> &'static str { "PUT /users/:id" }
async fn delete_user() -> &'static str { "DELETE /users/:id" }

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users", get(get_users).post(create_user))
        .route("/users/:id", get(get_user).put(update_user).delete(delete_user));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

ネスト

fn user_routes() -> Router {
    Router::new()
        .route("/", get(list_users).post(create_user))
        .route("/:id", get(get_user).put(update_user).delete(delete_user))
}

fn post_routes() -> Router {
    Router::new()
        .route("/", get(list_posts).post(create_post))
        .route("/:id", get(get_post))
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .nest("/users", user_routes())
        .nest("/posts", post_routes());

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

リクエストの処理

パスパラメータ

use axum::{Router, routing::get, extract::Path};

async fn get_user(Path(id): Path<u64>) -> String {
    format!("User ID: {}", id)
}

async fn get_post(Path((user_id, post_id)): Path<(u64, u64)>) -> String {
    format!("User: {}, Post: {}", user_id, post_id)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users/:id", get(get_user))
        .route("/users/:user_id/posts/:post_id", get(get_post));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
curl http://localhost:3000/users/42
# User ID: 42

curl http://localhost:3000/users/1/posts/100
# User: 1, Post: 100

クエリパラメータ

use axum::{Router, routing::get, extract::Query};
use serde::Deserialize;

#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    per_page: Option<u32>,
}

async fn list_users(Query(params): Query<Pagination>) -> String {
    let page = params.page.unwrap_or(1);
    let per_page = params.per_page.unwrap_or(10);
    format!("Page: {}, Per page: {}", page, per_page)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users", get(list_users));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
curl "http://localhost:3000/users?page=2&per_page=20"
# Page: 2, Per page: 20

JSON ボディ

use axum::{Router, routing::post, Json};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}

async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
    let user = User {
        id: 1,
        name: payload.name,
        email: payload.email,
    };
    Json(user)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users", post(create_user));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","email":"alice@example.com"}'
# {"id":1,"name":"Alice","email":"alice@example.com"}

ヘッダー

use axum::{Router, routing::get, http::HeaderMap};

async fn show_headers(headers: HeaderMap) -> String {
    let user_agent = headers
        .get("user-agent")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("Unknown");
    format!("User-Agent: {}", user_agent)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/headers", get(show_headers));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

レスポンスの返却

ステータスコード

use axum::{Router, routing::get, http::StatusCode};

async fn not_found() -> StatusCode {
    StatusCode::NOT_FOUND
}

async fn created() -> (StatusCode, &'static str) {
    (StatusCode::CREATED, "Created!")
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/404", get(not_found))
        .route("/201", get(created));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

ヘッダー付きレスポンス

use axum::{
    Router, routing::get,
    http::{StatusCode, header},
    response::IntoResponse,
};

async fn with_headers() -> impl IntoResponse {
    (
        StatusCode::OK,
        [(header::CONTENT_TYPE, "text/plain")],
        "Hello with headers!",
    )
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(with_headers));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

HTML

use axum::{Router, routing::get, response::Html};

async fn index() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

リダイレクト

use axum::{Router, routing::get, response::Redirect};

async fn redirect() -> Redirect {
    Redirect::to("/new-location")
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/old", get(redirect))
        .route("/new-location", get(|| async { "You were redirected!" }));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

エラーハンドリング

use axum::{
    Router, routing::get,
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;

// カスタムエラー型
enum AppError {
    NotFound,
    InternalError(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
            AppError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.leak()),
        };
        
        let body = Json(json!({ "error": message }));
        (status, body).into_response()
    }
}

async fn get_user(axum::extract::Path(id): axum::extract::Path<u64>) -> Result<Json<serde_json::Value>, AppError> {
    if id == 0 {
        return Err(AppError::NotFound);
    }
    Ok(Json(json!({ "id": id, "name": "Alice" })))
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users/:id", get(get_user));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

ミドルウェア

ロギング

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.5", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"
use axum::{Router, routing::get};
use tower_http::trace::TraceLayer;
use tracing_subscriber;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let app = Router::new()
        .route("/", get(|| async { "Hello!" }))
        .layer(TraceLayer::new_for_http());

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

CORS

[dependencies]
tower-http = { version = "0.5", features = ["cors"] }
use axum::{Router, routing::get};
use tower_http::cors::{CorsLayer, Any};

#[tokio::main]
async fn main() {
    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods(Any)
        .allow_headers(Any);

    let app = Router::new()
        .route("/", get(|| async { "Hello!" }))
        .layer(cors);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

状態の共有

use axum::{Router, routing::get, extract::State};
use std::sync::Arc;
use tokio::sync::RwLock;

struct AppState {
    counter: RwLock<u64>,
}

async fn increment(State(state): State<Arc<AppState>>) -> String {
    let mut counter = state.counter.write().await;
    *counter += 1;
    format!("Counter: {}", *counter)
}

async fn get_count(State(state): State<Arc<AppState>>) -> String {
    let counter = state.counter.read().await;
    format!("Counter: {}", *counter)
}

#[tokio::main]
async fn main() {
    let state = Arc::new(AppState {
        counter: RwLock::new(0),
    });

    let app = Router::new()
        .route("/increment", get(increment))
        .route("/count", get(get_count))
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

実践:REST API

完全な CRUD API の例:

use axum::{
    Router,
    routing::{get, post},
    extract::{Path, State, Json},
    http::StatusCode,
    response::IntoResponse,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;

#[derive(Clone, Serialize)]
struct Todo {
    id: u64,
    title: String,
    completed: bool,
}

#[derive(Deserialize)]
struct CreateTodo {
    title: String,
}

#[derive(Deserialize)]
struct UpdateTodo {
    title: Option<String>,
    completed: Option<bool>,
}

type Db = Arc<RwLock<HashMap<u64, Todo>>>;

async fn list_todos(State(db): State<Db>) -> Json<Vec<Todo>> {
    let todos = db.read().await;
    Json(todos.values().cloned().collect())
}

async fn create_todo(
    State(db): State<Db>,
    Json(input): Json<CreateTodo>,
) -> impl IntoResponse {
    let mut todos = db.write().await;
    let id = todos.len() as u64 + 1;
    let todo = Todo {
        id,
        title: input.title,
        completed: false,
    };
    todos.insert(id, todo.clone());
    (StatusCode::CREATED, Json(todo))
}

async fn get_todo(
    State(db): State<Db>,
    Path(id): Path<u64>,
) -> Result<Json<Todo>, StatusCode> {
    let todos = db.read().await;
    todos.get(&id).cloned().map(Json).ok_or(StatusCode::NOT_FOUND)
}

async fn update_todo(
    State(db): State<Db>,
    Path(id): Path<u64>,
    Json(input): Json<UpdateTodo>,
) -> Result<Json<Todo>, StatusCode> {
    let mut todos = db.write().await;
    let todo = todos.get_mut(&id).ok_or(StatusCode::NOT_FOUND)?;
    
    if let Some(title) = input.title {
        todo.title = title;
    }
    if let Some(completed) = input.completed {
        todo.completed = completed;
    }
    
    Ok(Json(todo.clone()))
}

async fn delete_todo(
    State(db): State<Db>,
    Path(id): Path<u64>,
) -> StatusCode {
    let mut todos = db.write().await;
    if todos.remove(&id).is_some() {
        StatusCode::NO_CONTENT
    } else {
        StatusCode::NOT_FOUND
    }
}

#[tokio::main]
async fn main() {
    let db: Db = Arc::new(RwLock::new(HashMap::new()));

    let app = Router::new()
        .route("/todos", get(list_todos).post(create_todo))
        .route("/todos/:id", get(get_todo).put(update_todo).delete(delete_todo))
        .with_state(db);

    println!("Server running on http://localhost:3000");
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
# 作成
curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Learn axum"}'

# 一覧
curl http://localhost:3000/todos

# 取得
curl http://localhost:3000/todos/1

# 更新
curl -X PUT http://localhost:3000/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"completed":true}'

# 削除
curl -X DELETE http://localhost:3000/todos/1

まとめ

axum vs 他のフレームワーク

特徴 axum actix-web rocket
マクロ 不要 必要 必要
tower 互換
学習曲線 緩やか 普通
型安全性 高い 高い 高い
パフォーマンス 高い 高い 普通

axum の魅力

  1. マクロ不要 - 関数がそのままハンドラ
  2. 型安全 - コンパイル時にエラー検出
  3. tower エコシステム - 豊富なミドルウェア
  4. tokio チーム製 - 安心感
  5. シンプル - 学習コストが低い

チェックリスト

  • Router::new() でルーター作成
  • Path, Query, Json でパラメータ抽出
  • State で状態共有
  • IntoResponse でカスタムレスポンス
  • tower-http でミドルウェア追加

axum、本当に神です。Rust で Web サーバー作るなら、まず axum を試してみてください!

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

20
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?