Warpとは何か
WarpはRustで書かれた軽量・高速なWebフレームワークです。Actix-webやAxumと並んでよく使われますが、Warpには独自の特徴があります。それが**フィルタ(Filter)**という概念です。
Warpでは「ルーティング」「リクエストの解析」「認証」などの処理をすべてフィルタとして定義し、それを組み合わせてアプリケーションを構築します。パズルのピースを組み合わせるようなイメージで、非常に柔軟な設計が可能です。
この記事では、Warpの基本的な使い方を実際のコード例とともに解説します。
セットアップ
まずCargoで新しいプロジェクトを作成し、Cargo.tomlに依存関係を追加します。
[package]
name = "warp-example"
version = "0.1.0"
edition = "2021"
[dependencies]
warp = "0.3"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Warpは内部でTokioを使った非同期処理を前提としているため、tokioも一緒に追加します。
最初のサーバーを起動する
まずはGET /helloにアクセスすると文字列を返す、シンプルなサーバーを作ってみましょう。
use warp::Filter;
#[tokio::main]
async fn main() {
// GETメソッド + "/hello" パスにマッチするフィルタ
let hello = warp::get()
.and(warp::path("hello"))
.map(|| "Hello, Warp!");
println!("サーバーを起動します: http://127.0.0.1:3030");
warp::serve(hello).run(([127, 0, 0, 1], 3030)).await;
}
warp::get()でGETリクエストにマッチするフィルタを作り、.and()で別のフィルタと合成しています。この「合成」がWarpの核心です。
パスパラメータを受け取る
URLのパスに含まれる動的な値(例: /users/42)を取り出すには、warp::path::param()を使います。
use warp::Filter;
#[tokio::main]
async fn main() {
// GET /users/:id
let user = warp::get()
.and(warp::path("users"))
.and(warp::path::param::<u32>()) // u32型として取り出す
.and(warp::path::end())
.map(|id: u32| {
format!("ユーザーID: {}", id)
});
warp::serve(user).run(([127, 0, 0, 1], 3030)).await;
}
warp::path::param::<u32>()は、パスの次のセグメントをu32として解析します。型が合わなければ自動的に404を返してくれる点も便利です。
JSONレスポンスを返す
実際のAPIではJSONを返すことがほとんどです。SerdeのDerive macroとWarpを組み合わせると簡単に実現できます。
use serde::Serialize;
use warp::Filter;
#[derive(Serialize)]
struct User {
id: u32,
name: String,
}
#[tokio::main]
async fn main() {
// GET /users/:id → JSONを返す
let user = warp::get()
.and(warp::path("users"))
.and(warp::path::param::<u32>())
.and(warp::path::end())
.map(|id: u32| {
let user = User {
id,
name: format!("ユーザー{}", id),
};
warp::reply::json(&user)
});
warp::serve(user).run(([127, 0, 0, 1], 3030)).await;
}
warp::reply::json()にSerializeを実装した構造体を渡すだけで、Content-Type: application/jsonヘッダ付きのレスポンスが返ります。
リクエストボディを受け取る
POSTリクエストでJSONボディを受け取るには、warp::body::json()フィルタを使います。
use serde::{Deserialize, Serialize};
use warp::Filter;
#[derive(Deserialize, Serialize)]
struct CreateUser {
name: String,
}
#[derive(Serialize)]
struct User {
id: u32,
name: String,
}
#[tokio::main]
async fn main() {
// POST /users
let create_user = warp::post()
.and(warp::path("users"))
.and(warp::path::end())
.and(warp::body::json::<CreateUser>()) // ボディをDeserialize
.map(|body: CreateUser| {
let user = User {
id: 1, // 実際はDBから採番
name: body.name,
};
warp::reply::with_status(
warp::reply::json(&user),
warp::http::StatusCode::CREATED, // 201を返す
)
});
warp::serve(create_user).run(([127, 0, 0, 1], 3030)).await;
}
warp::reply::with_status()でHTTPステータスコードを指定できます。
複数のルートを束ねる
実際のアプリでは複数のルートが必要です。.or()で複数のフィルタを束ねられます。
use warp::Filter;
#[tokio::main]
async fn main() {
let get_users = warp::get()
.and(warp::path("users"))
.and(warp::path::end())
.map(|| "ユーザー一覧");
let get_user = warp::get()
.and(warp::path("users"))
.and(warp::path::param::<u32>())
.and(warp::path::end())
.map(|id: u32| format!("ユーザー: {}", id));
// .or() でルートを合成する
let routes = get_users.or(get_user);
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}
Warpはマッチするフィルタを先頭から順に試みます。マッチしなければ次のフィルタを試すという動作になります。
まとめ
Warpの基本をまとめると以下のとおりです。
| 機能 | 使い方 |
|---|---|
| GETルート | warp::get().and(warp::path("xxx")) |
| パスパラメータ | warp::path::param::<T>() |
| JSONレスポンス | warp::reply::json(&data) |
| リクエストボディ | warp::body::json::<T>() |
| ルートの合成 | .or() |
| ステータスコード指定 | warp::reply::with_status() |
Warpのフィルタ合成という設計は最初は少し独特に感じますが、慣れると非常に柔軟で再利用しやすいコードが書けるようになります。まずは小さなAPIサーバーを作って、フィルタの組み合わせ方を試してみてください。