🏎️

Rust | Micro-framework "Ntex" で Web アプリを実装する

2024/03/31に公開

Ntex とは

https://ntex.rs/

Ntex is a powerful, pragmatic, and extremely fast framework for composable networking services for Rust

Ntexは、Rust向けのコンポーザブル・ネットワーキング・サービスのための、強力で実用的、そして非常に高速なフレームワークです。

公式ページでは、以下の 4 つの特徴が挙げられています。

  1. Type Safe: 型安全であること
  2. Feature Rich: 豊富な機能を提供していること
  3. Extensible: 拡張可能であること
  4. Blazingly Fast: 驚くほど高速に動作すること

高速に動作することという部分に関して、
Web Framework Benchmarks Round 22 で以下のランクを獲得しています。

  • 第 3 位: ntex [sailfish]
  • 第 14 位: ntex [async-std,db]

ちなみに axum [postgresql] は第 6 位でした。

なお、Ntex でサポートされている最小 Rust バージョン (MSRV) は 1.75 です。
(2024.03.31 時点 - Ntex 1.2.1)

Hello World!

公式ドキュメント を参考にしながら、プロトタイピングしていきたいと思います🔥🔥🔥

まずは Hello world! からはじめていきます!

Cargo.toml
[dependencies]
ntex = { version = "1.2.1", features = ["tokio"] }

マクロを使ったルーティング

Ntex ではマクロを使うことで、ルーティングできます。

main.rs
use ntex::web;

#[ntex::main]
async fn main() -> std::io::Result<()> {
    web::HttpServer::new(|| web::App::new().service(hello))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

#[web::get("/")]
async fn hello() -> impl web::Responder {
    web::HttpResponse::Ok().body("Hello world!")
}

http://127.0.0.1:8080/ に GET でアクセスすると、Hello world! と応答が返ってきます。

同じように POST /echo を実装すると、以下のようになります。

main.rs
use ntex::web;

#[ntex::main]
async fn main() -> std::io::Result<()> {
-    web::HttpServer::new(|| web::App::new().service(hello))
+    web::HttpServer::new(|| web::App::new().service(hello).service(echo))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

#[web::get("/")]
async fn hello() -> impl web::Responder {
    web::HttpResponse::Ok().body("Hello world!")
}

+ #[web::post("/echo")]
+ async fn echo(req_body: String) -> impl web::Responder {
+     web::HttpResponse::Ok().body(req_body)
+ }

マクロを使わないルーティング

マクロを使わないパターンでも実装が可能です。

main.rs
use ntex::web;

#[ntex::main]
async fn main() -> std::io::Result<()> {
    web::HttpServer::new(|| web::App::new().route("/", web::get().to(hello)))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

async fn hello() -> impl web::Responder {
    web::HttpResponse::Ok().body("Hello world!")
}

route() の引数にパスと web::get().to() で HTTP メソッドを指定したハンドラー関数を渡します。

このパターンだと、Axum と同じようにルーティングが実装できます。

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

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

    // build our application with a route
    let app = Router::new()
        // `GET /` goes to `root`
        .route("/", get(root));

    // run our app with hyper, listening globally on port 3000
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

// basic handler that responds with a static string
async fn root() -> &'static str {
    "Hello, World!"
}

Path パラメータを扱う

Type-safe な情報抽出を試してみたいと思います!

抽出可能なパスの部分は dynamic segments(動的セグメント) と呼ばれ、中括弧でマークします。
パスから任意の変数セグメントをデシリアライズできます。

web::types::Path で型を指定する

web::types::Path で型を指定することでパスから情報を抽出します。

複数のセグメントが存在する場合、宣言された順序でタプル型を定義します。

main.rs
use ntex::web;

#[ntex::main]
async fn main() -> std::io::Result<()> {
    web::HttpServer::new(|| web::App::new().service(welcome))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

#[web::get("/users/{user_id}/friends/{friend_name}")]
async fn welcome(path: web::types::Path<(u32, String)>) -> Result<String, web::Error> {
    let (user_id, friend_name) = path.into_inner();
    Ok(format!(
        "Welcome id: {}, friend name is {}!",
        user_id, friend_name
    ))
}

http://127.0.0.1:8080/users/100/friends/taro に GET でアクセスすると、HWelcome id: 100, friend name is taro! と応答が返ってきます。

http://127.0.0.1:8080/users/yamada/friends/taro の場合は Path deserialize error: can not parse "yamada" to a u32 が返り、デシリアライズに失敗したことが分かります。

Deserialize トレイトを使う

Serde の Deserialize トレイトを使うことで、パスから情報を抽出することも可能です。

Cargo.toml
[dependencies]
ntex = { version = "1.2.1", features = ["tokio"] }
serde = { version = "1.0.197", features = ["derive"] }
main.rs
use ntex::web;
use serde::Deserialize;

#[ntex::main]
async fn main() -> std::io::Result<()> {
    web::HttpServer::new(|| web::App::new().service(welcome))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

#[derive(Deserialize)]
struct MyInfo {
    user_id: u32,
    friend_name: String,
}

#[web::get("/users/{user_id}/friends/{friend_name}")]
async fn welcome(info: web::types::Path<MyInfo>) -> Result<String, web::Error> {
    Ok(format!(
        "Welcome id: {}, friend name is {}!",
        info.user_id, info.friend_name
    ))
}

Web アプリを構築するにあたり、Serde は使用は避けて通れないと思うので、
この実装の方がスッキリ書ける気がします。

JSON レスポンスを返す

JSON レスポンスを返すようにしてみます。

JSON を扱うために Serde を追加します。

Cargo.toml
[dependencies]
ntex = { version = "1.2.1", features = ["tokio"] }
serde = { version = "1.0.197", features = ["derive"] }
main.rs
use ntex::web;
use serde::Serialize;

#[ntex::main]
async fn main() -> std::io::Result<()> {
    web::HttpServer::new(|| web::App::new().service(hello))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

#[derive(Serialize)]
struct MyResponse {
    result: String,
}

#[web::get("/{name}")]
async fn hello(name: web::types::Path<String>) -> Result<impl web::Responder, web::Error> {
    let resp = MyResponse {
        result: format!("Hello {}!", name),
    };

    Ok(web::HttpResponse::Ok().json(&resp))
}

まとめ

個人的には Axum を推しているのですが、
Ntex も非常に使いやすそうな Web フレームワークだなと感じました。

高速というワードには、やはり惹きつけられるものがありますね...🫣✨

State を使って DB と接続できるようにするなど、より実践的な実装も試してみたいと思います!!

コラボスタイル Developers

Discussion