RustとMongoDBで作る簡単ToDoリストウェブアプリ

## 目次

1. はじめに
2. 前提条件
3. プロジェクトのセットアップ
4. MongoDBドライバのインストール
5. Actixウェブフレームワークの導入
6. MongoDBへの接続
7. ToDoモデルの作成
8. CRUD操作の実装
9. APIエンドポイントの作成
10. フロントエンドの実装
11. エラーハンドリング
12. テストの作成
13. デプロイメント
14. パフォーマンスの最適化
15. セキュリティの考慮事項
16. 結論

## 1. はじめに

この記事では、RustプログラミングとMongoDBを使用して、ウェブベースのToDoリストアプリケーションを作成する方法を詳しく説明します。バックエンドにはActixウェブフレームワークを使用し、フロントエンドには簡単なHTMLとJavaScriptを使用します。

## 2. 前提条件

このチュートリアルを進めるには、以下のものが必要です：

- Rustの基本的な知識
- 最新のRustツールチェーン（バージョン1.57+）
- MongoDBの基本的な知識とローカルにインストールされたMongoDBサーバー
- コードエディタ（VS CodeやIntelliJ Rustなど）
- ウェブ開発の基本的な知識

## 3. プロジェクトのセットアップ

新しいRustプロジェクトを作成します。

```bash
cargo new todo_list_web
cd todo_list_web
```

`Cargo.toml`ファイルに必要な依存関係を追加します。

```toml
[dependencies]
actix-web = "4.0"
mongodb = "2.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenv = "0.15"
thiserror = "1.0"
```

## 4. MongoDBドライバのインストール

MongoDBのRustドライバをインストールします。

```bash
cargo add mongodb
```

## 5. Actixウェブフレームワークの導入

Actixウェブフレームワークを導入します。`src/main.rs`に以下のコードを追加します。

```rust
use actix_web::{web, App, HttpServer, HttpResponse};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(|| HttpResponse::Ok().body("Hello, Todo List!")))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}
```

## 6. MongoDBへの接続

MongoDBに接続するためのコードを作成します。`src/db.rs`ファイルを作成し、以下のコードを追加します。

```rust
use mongodb::{Client, Database};
use dotenv::dotenv;
use std::env;

pub async fn connect_to_db() -> mongodb::error::Result<Database> {
    dotenv().ok();
    let uri = env::var("MONGODB_URI").expect("MONGODB_URI must be set");
    let client = Client::with_uri_str(&uri).await?;
    Ok(client.database("todo_db"))
}
```

## 7. ToDoモデルの作成

ToDoアイテムのモデルを作成します。`src/models.rs`ファイルを作成し、以下のコードを追加します。

```rust
use serde::{Deserialize, Serialize};
use mongodb::bson::oid::ObjectId;

#[derive(Debug, Serialize, Deserialize)]
pub struct Todo {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    pub id: Option<ObjectId>,
    pub title: String,
    pub completed: bool,
}
```

## 8. CRUD操作の実装

CRUD操作を実装します。`src/repository.rs`ファイルを作成し、以下のコードを追加します。

```rust
use mongodb::{Collection, Database};
use mongodb::bson::{doc, oid::ObjectId};
use crate::models::Todo;

pub struct TodoRepository {
    collection: Collection<Todo>,
}

impl TodoRepository {
    pub fn new(db: Database) -> Self {
        let collection = db.collection("todos");
        TodoRepository { collection }
    }

    pub async fn create_todo(&self, title: &str) -> mongodb::error::Result<ObjectId> {
        let new_todo = Todo {
            id: None,
            title: title.to_string(),
            completed: false,
        };
        let result = self.collection.insert_one(new_todo, None).await?;
        Ok(result.inserted_id.as_object_id().unwrap())
    }

    pub async fn get_todos(&self) -> mongodb::error::Result<Vec<Todo>> {
        let mut cursor = self.collection.find(None, None).await?;
        let mut todos = Vec::new();
        while let Some(todo) = cursor.try_next().await? {
            todos.push(todo);
        }
        Ok(todos)
    }

    pub async fn update_todo(&self, id: &ObjectId, completed: bool) -> mongodb::error::Result<()> {
        self.collection
            .update_one(doc! { "_id": id }, doc! { "$set": { "completed": completed } }, None)
            .await?;
        Ok(())
    }

    pub async fn delete_todo(&self, id: &ObjectId) -> mongodb::error::Result<()> {
        self.collection.delete_one(doc! { "_id": id }, None).await?;
        Ok(())
    }
}
```

## 9. APIエンドポイントの作成

APIエンドポイントを作成します。`src/handlers.rs`ファイルを作成し、以下のコードを追加します。

```rust
use actix_web::{web, HttpResponse, Responder};
use crate::repository::TodoRepository;
use crate::models::Todo;
use mongodb::bson::oid::ObjectId;

pub async fn create_todo(repo: web::Data<TodoRepository>, todo: web::Json<Todo>) -> impl Responder {
    match repo.create_todo(&todo.title).await {
        Ok(id) => HttpResponse::Created().json(id),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

pub async fn get_todos(repo: web::Data<TodoRepository>) -> impl Responder {
    match repo.get_todos().await {
        Ok(todos) => HttpResponse::Ok().json(todos),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

pub async fn update_todo(repo: web::Data<TodoRepository>, id: web::Path<String>, todo: web::Json<Todo>) -> impl Responder {
    let object_id = ObjectId::parse_str(&id).unwrap();
    match repo.update_todo(&object_id, todo.completed).await {
        Ok(_) => HttpResponse::Ok().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

pub async fn delete_todo(repo: web::Data<TodoRepository>, id: web::Path<String>) -> impl Responder {
    let object_id = ObjectId::parse_str(&id).unwrap();
    match repo.delete_todo(&object_id).await {
        Ok(_) => HttpResponse::NoContent().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}
```

## 10. フロントエンドの実装

簡単なフロントエンドを実装します。`static`ディレクトリを作成し、その中に`index.html`と`app.js`ファイルを作成します。

`index.html`:

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ToDoリスト</title>
</head>
<body>
    <h1>ToDoリスト</h1>
    <input type="text" id="todo-input" placeholder="新しいタスクを入力">
    <button onclick="addTodo()">追加</button>
    <ul id="todo-list"></ul>
    <script src="app.js"></script>
</body>
</html>
```

`app.js`:

```javascript
async function getTodos() {
    const response = await fetch('/todos');
    const todos = await response.json();
    const todoList = document.getElementById('todo-list');
    todoList.innerHTML = '';
    todos.forEach(todo => {
        const li = document.createElement('li');
        li.textContent = todo.title;
        li.onclick = () => toggleTodo(todo._id, !todo.completed);
        if (todo.completed) {
            li.style.textDecoration = 'line-through';
        }
        todoList.appendChild(li);
    });
}

async function addTodo() {
    const input = document.getElementById('todo-input');
    const title = input.value.trim();
    if (title) {
        await fetch('/todos', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({title})
        });
        input.value = '';
        getTodos();
    }
}

async function toggleTodo(id, completed) {
    await fetch(`/todos/${id}`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({completed})
    });
    getTodos();
}

getTodos();
```

## 11. エラーハンドリング

エラーハンドリングを改善します。`src/error.rs`ファイルを作成し、以下のコードを追加します。

```rust
use thiserror::Error;

#[derive(Error, Debug)]
pub enum TodoError {
    #[error("Database error: {0}")]
    DatabaseError(#[from] mongodb::error::Error),
    #[error("Invalid ID: {0}")]
    InvalidId(String),
}
```

## 12. テストの作成

単体テストと統合テストを作成します。`tests`ディレクトリを作成し、その中に`integration_tests.rs`ファイルを作成します。

```rust
#[cfg(test)]
mod tests {
    use actix_web::{test, web, App};
    use crate::handlers;
    use crate::repository::TodoRepository;
    use crate::db::connect_to_db;

    #[actix_rt::test]
    async fn test_get_todos() {
        let db = connect_to_db().await.unwrap();
        let repo = web::Data::new(TodoRepository::new(db));
        let app = test::init_service(
            App::new()
                .app_data(repo.clone())
                .route("/todos", web::get().to(handlers::get_todos))
        ).await;

        let req = test::TestRequest::get().uri("/todos").to_request();
        let resp = test::call_service(&app, req).await;
        assert!(resp.status().is_success());
    }
}
```

## 13. デプロイメント

アプリケーションをデプロイする方法について説明します。例えば、DockerとHerokuを使用したデプロイメント手順を提供します。

## 14. パフォーマンスの最適化

アプリケーションのパフォーマンスを最適化する方法について説明します。例えば、インデックスの使用、キャッシング、非同期処理の活用などを紹介します。

## 15. セキュリティの考慮事項

セキュリティに関する考慮事項について説明します。例えば、入力のバリデーション、認証と認可、HTTPS の使用、CORSの設定などを紹介します。

## 16. 結論

このチュートリアルでは、RustとMongoDBを使用してウェブベースのToDoリストアプリケーションを作成する方法を学びました。

Actixウェブフレームワークを使用してバックエンドを構築し、簡単なフロントエンドを実装しました。さらに、エラーハンドリング、テスト、デプロイメント、パフォーマンス最適化、セキュリティについても触れました。

このプロジェクトを基に、さらに機能を追加したり、より複雑なアプリケーションを構築したりすることができます。

RustとMongoDBの組み合わせは、高性能で信頼性の高いウェブアプリケーションを開発するための強力なツールセットです。

ぜひ、このチュートリアルを参考に、独自のアイデアを実現してください。Rustでのウェブ開発を楽しんでください！