0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

// 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
}

// 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を使用してタイプセーフなコードを実装

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?