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アプリで、以下の機能を実装しています:
機能
- タスク一覧の表示
- 新しいタスクの追加
- タスクの完了状態の切り替え
- タスクの削除
使い方
-
バックエンドの起動:
cd actix-backend cargo run
-
フロントエンドの起動:
cd nextjs-frontend npm install npm run dev
-
ブラウザで
http://localhost:3000
にアクセス
重要なポイント
- CORS設定: バックエンド側でCORS設定を行い、フロントエンドからのアクセスを許可
- 状態管理: バックエンドではMutexを使用してタスクリストの状態を管理
- エラーハンドリング: フロントエンドでは適切なエラーハンドリングを実装
- TypeScript: NextJSでTypeScriptを使用してタイプセーフなコードを実装
これを基盤として、認証機能やデータベース連携など、より高度な機能を追加していくことができます。