🦀
Rust x Actix Web x sqlx で TODO API
はじめに
Rust で TODO アプリようの API を作りました。
ソースコード
ここに置いてます。
使ったもの
Web フレームワークとして Actix Web を使いました。
Rust API で検索するとそこそこ情報出てくるので、そこそこ有名なのかなという印象です。
まだ DB 周りは、sqlx で sqlite にアクセスしてます。
ポイント
- sqlx のクエリを使うとき、NOT NULL になり得ると型が曖昧になので、明示的な sql を書いて解決してます。select 文に注目です。
- DB 接続プールもパスパラメーターやボディの値も引数に書くのでシンプルに見えます。
- ちゃんと CORS 用のミドルウェアもありました。とりあえず全無視で対応してます。
コード
こんな感じです。
Cargo.toml
Cargo.toml
[package]
name = "api"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4"
actix-cors = "0.6"
actix-rt = "2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.6", features = [
"runtime-actix-native-tls",
"sqlite",
"chrono",
] }
dotenv = "0.15"
uuid = { version = "1.1", features = ["v4"] }
.env
DATABASE_URL=sqlite://todos.db
sql
setup.sql
CREATE TABLE todos (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
main.rs
src/main.rs
use actix_cors::Cors;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use uuid::Uuid;
// Todo struct
#[derive(Serialize, Deserialize)]
struct Todo {
id: String,
title: String,
completed: bool,
}
// Todoのリクエスト用
#[derive(Deserialize)]
struct CreateTodo {
title: String,
}
// Updateのリクエスト用
#[derive(Deserialize)]
struct UpdateTodo {
title: String,
completed: bool,
}
// データベース接続の設定
async fn get_db_pool() -> SqlitePool {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
SqlitePool::connect(&database_url).await.unwrap()
}
// Todoを全て取得
async fn get_todos(pool: web::Data<SqlitePool>) -> impl Responder {
let todos = sqlx::query_as!(
Todo,
r#"SELECT id as "id!", title as "title!", completed FROM todos"#
)
.fetch_all(pool.get_ref())
.await
.unwrap();
HttpResponse::Ok().json(todos)
}
// 新しいTodoを作成
async fn create_todo(
pool: web::Data<SqlitePool>,
todo_data: web::Json<CreateTodo>,
) -> impl Responder {
let new_id = Uuid::new_v4().to_string();
// データベースに新しいTodoを挿入
sqlx::query!(
"INSERT INTO todos (id, title, completed) VALUES (?, ?, ?)",
new_id,
todo_data.title,
false,
)
.execute(pool.get_ref())
.await
.unwrap();
// 新しく作成されたTodoを作成して返す
let new_todo = Todo {
id: new_id.clone(),
title: todo_data.title.clone(),
completed: false,
};
// JSONレスポンスとして新しいTodoを返す
HttpResponse::Created().json(new_todo)
}
// Todoの更新処理
async fn update_todo(
pool: web::Data<SqlitePool>,
todo_id: web::Path<String>,
todo_data: web::Json<UpdateTodo>,
) -> impl Responder {
let id = todo_id.into_inner();
sqlx::query!(
"UPDATE todos SET title = ?, completed = ? WHERE id = ?",
todo_data.title,
todo_data.completed,
id,
)
.execute(pool.get_ref())
.await
.unwrap();
HttpResponse::Ok().body("Todo updated")
}
// Todoの削除処理
async fn delete_todo(pool: web::Data<SqlitePool>, todo_id: web::Path<String>) -> impl Responder {
let id = todo_id.into_inner();
sqlx::query!("DELETE FROM todos WHERE id = ?", id)
.execute(pool.get_ref())
.await
.unwrap();
HttpResponse::Ok().body("Todo deleted")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok();
let pool = get_db_pool().await;
HttpServer::new(move || {
App::new()
.wrap(
Cors::default()
.allow_any_origin() // 必要に応じて変更
.allow_any_method()
.allow_any_header()
.max_age(3600),
)
.app_data(web::Data::new(pool.clone()))
.route("/todos", web::get().to(get_todos))
.route("/todos", web::post().to(create_todo))
.route("/todos/{id}", web::put().to(update_todo))
.route("/todos/{id}", web::delete().to(delete_todo))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
手順
- sql でテーブル作成(
sqlite3 todos.db < setup.sql
) - cargo で run(
cargo run
) - curl 等で
127.0.0.1:8080
にアクセス確認
おわりに
他の言語と同じような感じでかけてストレス少なそうです。
そのうち ORM (Object Relational Mapper) を使ってみたいところです。
Discussion