Rustのaxumを使う

Rust熱が高まってきたのでaxumで遊んだ.やってることはactix-webの記事とほぼ同じ.

準備

ファイルの監視と自動コンパイル

必須ではないがコマンドwatchexec (https://github.com/watchexec/watchexec) を導入しておくとプログラムを書き換えたときにサーバを自動で再起動してもらえるようになって便利である.

watchexecはRust環境があればcargoでインストールできる:

cargo install --locked watchexec-cli

以下のように使用する:

watchexec -r -- cargo run

APIアクセスのテスト

curlコマンドとか使うのも良いけど, Insomnia (https://insomnia.rest/download) というアプリケーションを使うとすごく楽になった.

使用するクレート

cargo newでプロジェクトを作ったらCargo.toml[dependencies]に以下のクレートを書く:

uuid = { version = "1", features = ["v4", "serde"] }
axum = { version = "0.8", features = ["macros"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1", features = ["derive"] }
image = "0.25"

Hello, axum

axumでサーバを立てる最小限のプログラム.cargo runで実行し,ブラウザから http://localhost:3031 にアクセスすると「It works!」の文字列が表示される.

use axum::{
    routing::get,
    response::{Html, IntoResponse},
    Router,
};
use tokio::net::TcpListener;

async fn index() -> impl IntoResponse {
    Html("<h1>It works!</h1>")
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index));
    let listener = TcpListener::bind("localhost:3031").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

JSONをPOSTできるAPI

URLにユーザ名がPOSTされたらユーザを作成し,作成したユーザのIDと名前を返すAPIの例.

use axum::{
    routing::{get, post},
    response::{Html, Json, IntoResponse},
    Router,
};
use tokio::net::TcpListener;
use serde::{Serialize, Deserialize};
use uuid::Uuid;

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

#[derive(Serialize, Clone)]
struct User {
    id: Uuid,
    username: String,
}

async fn create_user(
    Json(payload): Json<CreateUser>
) -> impl IntoResponse {
    let user = User { id: Uuid::new_v4(), username: payload.username };
    Json(user)
}

async fn index() -> impl IntoResponse {
    Html("<h1>It works!</h1>").into_response()
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index))
        .route("/users", post(create_user));
    let listener = TcpListener::bind("localhost:3031").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

例えばJSON:

{
    "username": "example_user"
}

http://localhost:3031/users にPOSTすると以下のJSONが返ってくる.

{
    "id": "ae1e7bae-41dd-4f63-a32b-30df1eba9263",
    "username": "example_user"
}

画像を返すAPI

URL /image/{red} にアクセスが来たら画像を返すようなAPIを書く.画像を返すにはPngEncoderによってPNG形式にエンコードした画像データをベクタVec<u8>に入れて返す. MIME Type image/png も忘れずつける

use axum::{
    routing::get,
    extract::Path,
    http::{StatusCode, header},
    response::{Html, IntoResponse},
    Router,
};
use tokio::net::TcpListener;
use image::{
    codecs::png::PngEncoder,
    ImageEncoder, ColorType, RgbImage
};

async fn image(Path(r): Path<u8>) -> impl IntoResponse {
    let mut img = RgbImage::new(512, 512);
    for x in 0..img.width() {
        for y in 0..img.height() {
            let g = 255 - (x as f32 / img.width() as f32 * 255.0) as u8;
            let b = (y as f32 / img.width() as f32 * 255.0) as u8;
            img.put_pixel(x, y, image::Rgb([r, g, b]));
        }
    }
    let mut buf = std::io::Cursor::new(Vec::<u8>::new());
    let encoder = PngEncoder::new(&mut buf);
    encoder.write_image(&img, img.width(), img.height(), ColorType::Rgb8.into()).unwrap();
    let image_content = buf.into_inner();

    (
        StatusCode::OK,
        [(header::CONTENT_TYPE, "image/png")],
        image_content
    )
}

async fn index() -> impl IntoResponse {
    Html("<h1>It works!</h1>")
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index))
        .route("/image/{red}", get(image));
    let listener = TcpListener::bind("localhost:3031").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

URL http://localhost:3031/image/128 にアクセスすると以下の画像が返ってくる.

An example return value of image api

おわり

axumは全体的に記述がすっきりしてて良いですね