Rust | Micro-framework "Ntex" で Web アプリを実装する
Ntex とは
Ntex is a powerful, pragmatic, and extremely fast framework for composable networking services for Rust
Ntexは、Rust向けのコンポーザブル・ネットワーキング・サービスのための、強力で実用的、そして非常に高速なフレームワークです。
公式ページでは、以下の 4 つの特徴が挙げられています。
- Type Safe: 型安全であること
- Feature Rich: 豊富な機能を提供していること
- Extensible: 拡張可能であること
- 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! からはじめていきます!
[dependencies]
ntex = { version = "1.2.1", features = ["tokio"] }
マクロを使ったルーティング
Ntex ではマクロを使うことで、ルーティングできます。
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
を実装すると、以下のようになります。
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)
+ }
マクロを使わないルーティング
マクロを使わないパターンでも実装が可能です。
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
で型を指定することでパスから情報を抽出します。
複数のセグメントが存在する場合、宣言された順序でタプル型を定義します。
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 トレイトを使うことで、パスから情報を抽出することも可能です。
[dependencies]
ntex = { version = "1.2.1", features = ["tokio"] }
serde = { version = "1.0.197", features = ["derive"] }
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 を追加します。
[dependencies]
ntex = { version = "1.2.1", features = ["tokio"] }
serde = { version = "1.0.197", features = ["derive"] }
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 と接続できるようにするなど、より実践的な実装も試してみたいと思います!!
Discussion