目的
AWS LambdaでAPIっぽいことをするにはlambda_httpを使うことになるんですが、HTTPの低レイヤーを相手にすることが多くて辛いです。
何かいいものが無いかと調べていたら、数日前にaxum-aws-lambdaというものが出来ていたのでこれを使ってみました(この記事を書いた日からだと27日前でした)。
コード
lambda_httpだけでやってみる。
Cargo.toml
[package]
name = "web"
version = "0.1.0"
edition = "2021"
[dependencies]
lambda_http = "0.5.2"
serde_json = "1"
tokio = { version = "1", features = ["full"] }
main.rs
use lambda_http::{
    http::{response::Builder, Method, StatusCode},
    service_fn, Body, Error, Request, RequestExt, Response,
};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Error> {
    lambda_http::run(service_fn(func)).await?;
    Ok(())
}
async fn func(request: Request) -> Result<Response<Body>, Error> {
    let unmatch = not_found();
    match request.uri().path() {
        "/name" => match request.method() {
            &Method::GET => execute_get(&request).await,
            &Method::POST => execute_post(&request).await,
            _ => unmatch,
        },
        _ => unmatch,
    }
}
fn not_found() -> Result<Response<Body>, Error> {
    let builder = Builder::new().status(StatusCode::NOT_FOUND);
    let json = json!({});
    Ok(builder.body(Body::Text(json.to_string()))?)
}
async fn execute_get(request: &Request) -> Result<Response<Body>, Error> {
    let params = request.query_string_parameters();
    let name = params.first("name").unwrap_or("World");
    let builder = Builder::new().status(StatusCode::OK);
    let json = json!({
        "type": "get",
        "message": format!("{} World!", name)
    });
    Ok(builder.body(Body::Text(json.to_string()))?)
}
async fn execute_post(request: &Request) -> Result<Response<Body>, Error> {
    let json: serde_json::Value = match request.body() {
        Body::Text(text) => serde_json::from_str(text).unwrap_or(serde_json::Value::Null),
        _ => serde_json::Value::Null,
    };
    let builder = Builder::new().status(StatusCode::OK);
    let json = json!({
        "type": "post",
        "message": format!("{} World!", json["name"].as_str().unwrap_or("World"))
    });
    Ok(builder.body(Body::Text(json.to_string()))?)
}
辛いところ
- ルーティングするためにパスやメソッドを分析
- not foundの準備
- クエリーパラメーターを分解
- リクエストBodyの解析
- レスポンスを作る
aws-lambda-axumを使う
Cargo.toml
[package]
name = "web"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.5.11"
axum-aws-lambda = "0.1.0"
lambda_http = "0.5.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
 main.rs
use axum::{Router, routing::{get, post}, response::IntoResponse, Json, extract::Query};
use serde::Deserialize;
use serde_json::json;
#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(execute_get))
        .route("/", post(execute_post))
    ;
    let app = lambda_http::tower::ServiceBuilder::new()
        .layer(axum_aws_lambda::LambdaLayer::default())
        .service(app);
    lambda_http::run(app).await.unwrap();
}
#[derive(Deserialize)]
pub struct GetParameter {
    name: String,
}
async fn execute_get(Query(params): Query<GetParameter>) -> impl IntoResponse {
    Json(json!({
        "type": "get",
        "message": format!("{} Worled!", params.name)
    }))
}
#[derive(Deserialize)]
pub struct PostParameter {
    name: String,
}
async fn execute_post(Json(params): Json<PostParameter>) -> impl IntoResponse {
    Json(json!({
        "type": "post",
        "message": format!("{} Worled!", params.name)
    }))
}
まとめ
辛いとこまとめて解決できました。
lambda_httpもaxumもtower-layerを利用いるので相性が良いようです。
axum-aws-lambdaのサンプルでは他のtower-layerも使っていて、CORSなどの機能を簡単に追加しています。
自分もtower-cookieを利用していますが問題無く使えています。
