🥃
Rust | Axum で API のバージョンを管理する(API Versioning)
Axum API Versioning
https://api.example.com/v1/users
の v1
のように、パスパラメータにバージョン情報を付与していきます。
サンプルコード
以下の記事で作成した Axum プロジェクトに API Versoning の処理を実装します。
cargo.toml
async-trait を追加しました。
cargo.toml
[dependencies]
axum = "0.7.4"
tokio = { version = "1.0", features = ["full"] }
async-trait = { version = "0.1.77", features = [] }
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.96"
main.rs
今回のコード全体です。
以下のエンドポイントが実装されています。
/v1/hc
/v1/mountains
main.rs
use axum::{
async_trait,
extract::{FromRequestParts, Path},
http::{request::Parts, StatusCode},
response::{IntoResponse, Response},
routing::get,
Json, RequestPartsExt, Router,
};
use serde::Serialize;
use std::collections::HashMap;
#[tokio::main]
async fn main() {
let hc_router = Router::new().route("/", get(health_check));
let mountain_router = Router::new().route("/", get(find_sacred_mountains));
let app = Router::new()
.nest("/:version/hc", hc_router)
.nest("/:version/mountains", mountain_router);
// run it
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
.await
.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app)
.await
.unwrap_or_else(|_| panic!("Server cannot launch."));
}
async fn health_check(version: ApiVersion) -> StatusCode {
println!("received request with version {:?}", version);
StatusCode::OK
}
async fn find_sacred_mountains(version: ApiVersion) -> (StatusCode, Json<JsonResponse>) {
println!("received request with version {:?}", version);
let response: JsonResponse = Default::default();
(StatusCode::OK, Json(response))
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonResponse {
mountains: Vec<EightThousander>,
total: usize,
}
impl Default for JsonResponse {
fn default() -> Self {
let mountains = vec![
EightThousander::new(1, "エベレスト".to_string(), 8848),
EightThousander::new(2, "K2".to_string(), 8611),
EightThousander::new(3, "カンチェンジェンガ".to_string(), 8586),
EightThousander::new(4, "ローツェ".to_string(), 8516),
EightThousander::new(5, "マカルー".to_string(), 8463),
EightThousander::new(6, "チョ・オユー".to_string(), 8188),
EightThousander::new(7, "ダウラギリ".to_string(), 8167),
EightThousander::new(8, "マナスル".to_string(), 8163),
EightThousander::new(9, "ナンガ・パルバット".to_string(), 8126),
EightThousander::new(10, "アンナプルナ".to_string(), 8091),
EightThousander::new(11, "ガッシャーブルⅠ峰".to_string(), 8080),
EightThousander::new(12, "ブロード・ピーク".to_string(), 8051),
EightThousander::new(13, "ガッシャーブルムⅡ峰".to_string(), 8035),
EightThousander::new(14, "シシャパンマ".to_string(), 8027),
];
let total = mountains.len();
Self { mountains, total }
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EightThousander {
id: i32,
name: String,
elevation: i32,
}
impl EightThousander {
fn new(id: i32, name: String, elevation: i32) -> Self {
Self {
id,
name,
elevation,
}
}
}
#[derive(Debug)]
enum ApiVersion {
V1,
V2,
V3,
}
#[async_trait]
impl<S> FromRequestParts<S> for ApiVersion
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let params: Path<HashMap<String, String>> =
parts.extract().await.map_err(IntoResponse::into_response)?;
let version = params
.get("version")
.ok_or_else(|| (StatusCode::NOT_FOUND, "version param missing").into_response())?;
match version.as_str() {
"v1" => Ok(ApiVersion::V1),
"v2" => Ok(ApiVersion::V2),
"v3" => Ok(ApiVersion::V3),
_ => Err((StatusCode::NOT_FOUND, "unknown version").into_response()),
}
}
}
FromRequestParts
Versioning に関する処理を見ていきます。
まず、バージョンを enum で定義します。
#[derive(Debug)]
enum ApiVersion {
V1,
V2,
V3,
}
axum::extract::FromRequestParts
trait を実装することで、
パスパラメータで渡ってきた version を検証します。
#[async_trait]
impl<S> FromRequestParts<S> for ApiVersion
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let params: Path<HashMap<String, String>> =
parts.extract().await.map_err(IntoResponse::into_response)?;
let version = params
.get("version")
.ok_or_else(|| (StatusCode::NOT_FOUND, "version param missing").into_response())?;
match version.as_str() {
"v1" => Ok(ApiVersion::V1),
"v2" => Ok(ApiVersion::V2),
"v3" => Ok(ApiVersion::V3),
_ => Err((StatusCode::NOT_FOUND, "unknown version").into_response()),
}
}
}
ハンドラ関数の引数に、実装した ApiVersion を追加します。
引数を追加することで、バージョンの検証処理を通すことができます。
- async fn find_sacred_mountains() -> (StatusCode, Json<JsonResponse>) {
+ async fn find_sacred_mountains(version: ApiVersion) -> (StatusCode, Json<JsonResponse>) {
println!("received request with version {:?}", version);
let response: JsonResponse = Default::default();
(StatusCode::OK, Json(response))
}
println!
で version
を出力していますが、
特に処理をしない場合は _
に変えておくと、未使用の warning が出なくなります。
async fn find_sacred_mountains(_: ApiVersion) -> (StatusCode, Json<JsonResponse>) {
println!("received request with version {:?}", version);
let response: JsonResponse = Default::default();
(StatusCode::OK, Json(response))
}
まとめ
Axum で API Versioning する方法を試してみました。
大事なのは... 公式の Examples を見ること!!
Discussion