RustをバックエンドでActix Webを使いNextJSをフロントエンドに使う

Actix WebとNextJSを連携させるサンプルコードをバックエンドとフロントエンドの両方でご紹介します。

```rust
// Cargo.toml
[package]
name = "actix-backend"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.4.0"
actix-cors = "0.6.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
env_logger = "0.10.0"
log = "0.4.20"

// src/main.rs
use actix_cors::Cors;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
use log::info;

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Task {
    id: u32,
    title: String,
    completed: bool,
}

// アプリケーションのステートを管理
struct AppState {
    tasks: web::Data<std::sync::Mutex<Vec<Task>>>,
}

async fn get_tasks(data: web::Data<std::sync::Mutex<Vec<Task>>>) -> impl Responder {
    let tasks = data.lock().unwrap();
    HttpResponse::Ok().json(tasks.clone())
}

#[derive(Deserialize)]
struct CreateTask {
    title: String,
}

async fn create_task(
    data: web::Data<std::sync::Mutex<Vec<Task>>>,
    task: web::Json<CreateTask>,
) -> impl Responder {
    let mut tasks = data.lock().unwrap();
    let id = tasks.len() as u32 + 1;
    
    let new_task = Task {
        id,
        title: task.title.clone(),
        completed: false,
    };
    
    tasks.push(new_task.clone());
    HttpResponse::Created().json(new_task)
}

async fn toggle_task(
    data: web::Data<std::sync::Mutex<Vec<Task>>>,
    path: web::Path<u32>,
) -> impl Responder {
    let task_id = path.into_inner();
    let mut tasks = data.lock().unwrap();
    
    for task in tasks.iter_mut() {
        if task.id == task_id {
            task.completed = !task.completed;
            return HttpResponse::Ok().json(task.clone());
        }
    }
    
    HttpResponse::NotFound().body(format!("Task with ID {} not found", task_id))
}

async fn delete_task(
    data: web::Data<std::sync::Mutex<Vec<Task>>>,
    path: web::Path<u32>,
) -> impl Responder {
    let task_id = path.into_inner();
    let mut tasks = data.lock().unwrap();
    
    let initial_len = tasks.len();
    tasks.retain(|task| task.id != task_id);
    
    if tasks.len() != initial_len {
        HttpResponse::Ok().body(format!("Task with ID {} deleted", task_id))
    } else {
        HttpResponse::NotFound().body(format!("Task with ID {} not found", task_id))
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
    
    // 初期タスクの作成
    let tasks = web::Data::new(std::sync::Mutex::new(vec![
        Task {
            id: 1,
            title: "Rustの勉強".to_string(),
            completed: false,
        },
        Task {
            id: 2,
            title: "Actix Webの学習".to_string(),
            completed: false,
        },
        Task {
            id: 3,
            title: "NextJSとの連携".to_string(),
            completed: false,
        },
    ]));
    
    info!("Starting HTTP server at http://localhost:8080");
    
    HttpServer::new(move || {
        // CORSの設定
        let cors = Cors::default()
            .allow_any_origin()
            .allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
            .allowed_headers(vec!["Content-Type"])
            .max_age(3600);
        
        App::new()
            .wrap(cors)
            .app_data(tasks.clone())
            .route("/api/tasks", web::get().to(get_tasks))
            .route("/api/tasks", web::post().to(create_task))
            .route("/api/tasks/{id}", web::put().to(toggle_task))
            .route("/api/tasks/{id}", web::delete().to(delete_task))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

```

```typescript
// pages/index.tsx
import { useState, useEffect } from 'react';
import Head from 'next/head';
import styles from '../styles/Home.module.css';

interface Task {
  id: number;
  title: string;
  completed: boolean;
}

export default function Home() {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [newTaskTitle, setNewTaskTitle] = useState('');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // タスク一覧を取得
  const fetchTasks = async () => {
    try {
      setLoading(true);
      const response = await fetch('http://localhost:8080/api/tasks');
      if (!response.ok) {
        throw new Error('タスクの取得に失敗しました');
      }
      const data = await response.json();
      setTasks(data);
      setError(null);
    } catch (err) {
      setError('サーバーとの通信に失敗しました');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  // 初回レンダリング時にタスクを取得
  useEffect(() => {
    fetchTasks();
  }, []);

  // 新しいタスクを追加
  const addTask = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!newTaskTitle.trim()) return;

    try {
      const response = await fetch('http://localhost:8080/api/tasks', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ title: newTaskTitle }),
      });

      if (!response.ok) {
        throw new Error('タスクの追加に失敗しました');
      }

      const newTask = await response.json();
      setTasks([...tasks, newTask]);
      setNewTaskTitle('');
      setError(null);
    } catch (err) {
      setError('タスクの追加に失敗しました');
      console.error(err);
    }
  };

  // タスクの完了状態を切り替え
  const toggleTask = async (id: number) => {
    try {
      const response = await fetch(`http://localhost:8080/api/tasks/${id}`, {
        method: 'PUT',
      });

      if (!response.ok) {
        throw new Error('タスクの更新に失敗しました');
      }

      const updatedTask = await response.json();
      setTasks(tasks.map(task => 
        task.id === updatedTask.id ? updatedTask : task
      ));
      setError(null);
    } catch (err) {
      setError('タスクの更新に失敗しました');
      console.error(err);
    }
  };

  // タスクを削除
  const deleteTask = async (id: number) => {
    try {
      const response = await fetch(`http://localhost:8080/api/tasks/${id}`, {
        method: 'DELETE',
      });

      if (!response.ok) {
        throw new Error('タスクの削除に失敗しました');
      }

      setTasks(tasks.filter(task => task.id !== id));
      setError(null);
    } catch (err) {
      setError('タスクの削除に失敗しました');
      console.error(err);
    }
  };

  return (
    <div className={styles.container}>
      <Head>
        <title>Rust + NextJS Todo App</title>
        <meta name="description" content="Rust Actix Web + NextJS Todo App" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Rust + NextJS Todo App
        </h1>

        {error && <p className={styles.error}>{error}</p>}

        <form onSubmit={addTask} className={styles.form}>
          <input
            type="text"
            value={newTaskTitle}
            onChange={(e) => setNewTaskTitle(e.target.value)}
            placeholder="新しいタスクを入力..."
            className={styles.input}
          />
          <button type="submit" className={styles.button}>追加</button>
        </form>

        {loading ? (
          <p>Loading...</p>
        ) : (
          <ul className={styles.taskList}>
            {tasks.map((task) => (
              <li key={task.id} className={styles.task}>
                <span
                  className={`${styles.taskTitle} ${task.completed ? styles.completed : ''}`}
                  onClick={() => toggleTask(task.id)}
                >
                  {task.title}
                </span>
                <button 
                  onClick={() => deleteTask(task.id)}
                  className={styles.deleteButton}
                >
                  削除
                </button>
              </li>
            ))}
          </ul>
        )}
      </main>
    </div>
  );
}

// styles/Home.module.css
.container {
  padding: 0 2rem;
}

.main {
  min-height: 100vh;
  padding: 4rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
}

.title {
  margin: 0;
  line-height: 1.15;
  font-size: 2rem;
  margin-bottom: 2rem;
}

.error {
  color: red;
  margin-bottom: 1rem;
}

.form {
  display: flex;
  width: 100%;
  max-width: 500px;
  margin-bottom: 2rem;
}

.input {
  flex: 1;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px 0 0 4px;
}

.button {
  padding: 0.5rem 1rem;
  background: #0070f3;
  color: white;
  border: none;
  border-radius: 0 4px 4px 0;
  cursor: pointer;
}

.button:hover {
  background: #0051bb;
}

.taskList {
  list-style: none;
  padding: 0;
  width: 100%;
  max-width: 500px;
}

.task {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
  margin-bottom: 0.5rem;
  background: #f9f9f9;
  border-radius: 4px;
}

.taskTitle {
  cursor: pointer;
  flex: 1;
}

.completed {
  text-decoration: line-through;
  color: #888;
}

.deleteButton {
  background: #ff4d4f;
  color: white;
  border: none;
  padding: 0.3rem 0.6rem;
  border-radius: 4px;
  cursor: pointer;
}

.deleteButton:hover {
  background: #cf1322;
}

```

上記のコードは、RustのActix Webフレームワークを使ったバックエンドと、NextJSを使ったフロントエンドの連携サンプルです。これは簡単なTodoアプリで、以下の機能を実装しています：

### 機能
1. タスク一覧の表示
2. 新しいタスクの追加
3. タスクの完了状態の切り替え
4. タスクの削除

### 使い方
1. バックエンドの起動:
   ```
   cd actix-backend
   cargo run
   ```

2. フロントエンドの起動:
   ```
   cd nextjs-frontend
   npm install
   npm run dev
   ```

3. ブラウザで `http://localhost:3000` にアクセス

### 重要なポイント
1. **CORS設定**: バックエンド側でCORS設定を行い、フロントエンドからのアクセスを許可
2. **状態管理**: バックエンドではMutexを使用してタスクリストの状態を管理
3. **エラーハンドリング**: フロントエンドでは適切なエラーハンドリングを実装
4. **TypeScript**: NextJSでTypeScriptを使用してタイプセーフなコードを実装

これを基盤として、認証機能やデータベース連携など、より高度な機能を追加していくことができます。