みなさん、こんにちは!
前回、React(クライアント)とNode.js(サーバー)を連携させ、タスクの「取得(GET)」と「作成(POST)」を実装しましたね。
前回の記事
これでアプリは大きく進化しましたが、まだ一つ大きな問題が残っています。
それは、「タスクのデータが消えてしまう」ことです。
現在のタスクデータはserver.js
内のメモリ(tasks
配列)に一時的に保存されているだけで、サーバーを再起動するたびにデータがリセットされてしまいます。これでは実用的なアプリケーションとは言えません。
今回は、この問題を解決するデータの永続化を実装します。
Node.jsの機能を使ってタスクデータをJSONファイルに保存し、さらにタスクの完了状態を切り替える「更新(PATCH)」と、不要なタスクを消す「削除(DELETE)」の処理も実装して、Webサービスの基本となるCRUD操作を完成させます。
記事のゴール
- Node.jsの
fs
(ファイルシステム)モジュールを使って、データをファイルに保存・読み込みする - サーバーを再起動してもデータが消えない「永続化」を達成する
- 既存のタスクを更新するPATCHリクエストを学ぶ
- 既存のタスクを削除するDELETEリクエストを学ぶ
- Webサービスの基本操作CRUDを完全に実装する
1. サーバー側の準備:データの永続化とCRUD APIの実装
1.1. tasks.json
ファイルの作成
my-api-server
フォルダ直下に、tasks.json
というファイルを作成し、以下の初期データを貼り付けてください。
[
{ "id": 1, "title": "Node.jsの勉強", "completed": false },
{ "id": 2, "title": "Reactの記事を読む", "completed": false },
{ "id": 3, "title": "休憩する", "completed": true }
]
1.2. server.js
の修正(CRUDと永続化の追加)
server.js
の中身をすべて以下のコードに書き換えてください。
// server.js
const express = require('express');
const cors = require('cors');
const fs = require('fs'); // ファイルシステムモジュールをインポート
const path = require('path'); // ファイルパスを扱うモジュール
const app = express();
const port = 3000;
// データファイルのパス
const DATA_FILE = path.join(__dirname, 'tasks.json');
// --- データの読み書き関数 ---
// ファイルからタスクを読み込む関数
const loadTasks = () => {
try {
const data = fs.readFileSync(DATA_FILE, 'utf8');
return JSON.parse(data);
} catch (err) {
// ファイルが存在しない場合や読み込みエラーの場合、空の配列を返す
console.error('タスクファイルの読み込みに失敗しました:', err.message);
return [];
}
};
// タスクをファイルに書き込む関数
const saveTasks = (tasks) => {
// JSON.stringifyの第3引数「2」は、JSONを見やすく整形するためのものです
fs.writeFileSync(DATA_FILE, JSON.stringify(tasks, null, 2), 'utf8');
};
// 初期タスクデータをファイルから読み込む
let tasks = loadTasks();
let nextId = tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1; // 次のIDを計算
// --- ミドルウェアの設定 ---
app.use(cors());
app.use(express.json());
// --- APIエンドポイントの定義 ---
// 1. 全タスクを取得するAPI (GET /api/tasks) - READ
app.get('/api/tasks', (req, res) => {
res.json(tasks);
});
// 2. 新しいタスクを追加するAPI (POST /api/tasks) - CREATE
app.post('/api/tasks', (req, res) => {
const newTaskTitle = req.body.title;
if (!newTaskTitle) {
return res.status(400).json({ message: 'タスクのタイトルが必要です' });
}
const newTask = {
id: nextId++,
title: newTaskTitle,
completed: false
};
tasks.push(newTask);
saveTasks(tasks); // データをファイルに保存
res.status(201).json(newTask);
});
// 3. タスクを更新するAPI (PATCH /api/tasks/:id) - UPDATE
app.patch('/api/tasks/:id', (req, res) => {
const taskId = parseInt(req.params.id, 10);
const taskIndex = tasks.findIndex(t => t.id === taskId);
if (taskIndex === -1) {
return res.status(404).json({ message: '指定されたタスクは見つかりません' });
}
// 完了状態(completed)をトグル(反転)する
tasks[taskIndex].completed = !tasks[taskIndex].completed;
saveTasks(tasks); // データをファイルに保存
res.json(tasks[taskIndex]);
});
// 4. タスクを削除するAPI (DELETE /api/tasks/:id) - DELETE
app.delete('/api/tasks/:id', (req, res) => {
const taskId = parseInt(req.params.id, 10);
const initialLength = tasks.length;
// filterを使って、指定されたID以外のタスクで新しい配列を作成
tasks = tasks.filter(t => t.id !== taskId);
if (tasks.length === initialLength) {
return res.status(404).json({ message: '指定されたタスクは見つかりません' });
}
saveTasks(tasks); // データをファイルに保存
// 成功時はコンテンツなしを示す204を返す
res.status(204).send();
});
// サーバーを起動します
app.listen(port, () => {
console.log(`サーバーが http://localhost:${port} で起動しました`);
});
【server.js
のコード解説】
-
データ永続化の仕組み
-
fs
モジュール: Node.js標準の機能で、ファイルシステム(ファイルの読み書き)を扱います。 -
loadTasks()
: サーバー起動時にtasks.json
ファイルからデータを読み込み、JavaScriptのtasks
配列に格納します。これにより、サーバーを再起動してもデータが保持されます。 -
saveTasks(tasks)
:POST
,PATCH
,DELETE
などのデータ更新が行われた直後に実行されます。最新のtasks
配列の内容をファイルに書き込み、変更を永続化します。
-
-
CRUD操作のエンドポイント
-
GET (READ):
app.get('/api/tasks', ...)
。tasks
配列の内容をそのままクライアントに返します。 -
POST (CREATE):
app.post('/api/tasks', ...)
。新しいタスクを追加した後、必ずsaveTasks()
を呼び出してファイルに書き込みます。 -
PATCH (UPDATE):
app.patch('/api/tasks/:id', ...)
。-
req.params.id
でURLのIDを取得し、更新対象のタスクを見つけます。 - タスクの
completed
プロパティを反転(完了 $\Leftrightarrow$ 未完了)させます。 - 更新後、
saveTasks()
を呼び出します。
-
-
DELETE (DELETE):
app.delete('/api/tasks/:id', ...)
。-
filter
関数を使い、削除したいID以外のタスクで新しい配列を作り直します。 - 成功を示す**
204 No Content
**ステータスを返します。
-
-
GET (READ):
2. クライアント側の準備:更新と削除(DELETE)の実装
my-react-app
フォルダのsrc/App.jsx
の中身をすべて以下のコードに書き換えてください。
// src/App.jsx
import React, { useState, useEffect } from 'react';
import './App.css';
const API_URL = 'http://localhost:3000/api/tasks';
function App() {
const [tasks, setTasks] = useState([]);
const [newTaskTitle, setNewTaskTitle] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// --- 1. GETリクエスト(タスク一覧の取得) ---
const fetchTasks = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error(`HTTPエラー! ステータス: ${response.status}`);
}
const data = await response.json();
setTasks(data);
} catch (err) {
setError('タスクの取得に失敗しました。');
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTasks();
}, []);
// --- 2. POSTリクエスト(タスクの新規作成) ---
const handleAddTask = async () => {
if (!newTaskTitle.trim()) return;
try {
const response = await fetch(API_URL, {
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('');
} catch (err) {
setError(err.message);
console.error(err);
}
};
// --- 3. PATCHリクエスト(タスクの更新) ---
const handleToggleCompleted = async (taskId) => {
try {
const response = await fetch(`${API_URL}/${taskId}`, {
method: 'PATCH',
});
if (!response.ok) {
throw new Error('タスクの更新に失敗しました。');
}
const updatedTask = await response.json();
setTasks(tasks.map(task =>
task.id === taskId ? updatedTask : task
));
} catch (err) {
setError(err.message);
console.error(err);
}
};
// --- 4. DELETEリクエスト(タスクの削除) ---
const handleDeleteTask = async (taskId) => {
try {
const response = await fetch(`${API_URL}/${taskId}`, {
method: 'DELETE', // HTTPメソッドをDELETEに指定
});
// サーバーが204を返しているため、200番台であれば成功とみなす
if (response.status !== 204) {
throw new Error('タスクの削除に失敗しました。');
}
// 成功した場合、フロントエンドのリストからも該当タスクを削除
setTasks(tasks.filter(task => task.id !== taskId));
} catch (err) {
setError(err.message);
console.error(err);
}
};
return (
<div className="App">
<h1>My Task App - CRUD完全版</h1>
{/* エラー表示 */}
{error && <p style={{ color: 'red' }}>{error}</p>}
{/* タスク新規作成フォーム (POST) */}
<div className="task-form">
<input
type="text"
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
placeholder="新しいタスクを入力"
/>
<button onClick={handleAddTask} disabled={loading}>タスクを追加 (POST)</button>
</div>
<hr />
{/* タスク一覧表示 (GETの結果) */}
<h2>タスク一覧</h2>
{loading ? (
<p>タスクを読み込み中...</p>
) : (
<ul>
{tasks.map(task => (
<li key={task.id} style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ textDecoration: task.completed ? 'line-through' : 'none', flexGrow: 1, marginRight: '10px' }}>
[{task.id}] {task.title}
</span>
<button onClick={() => handleToggleCompleted(task.id)} style={{ marginRight: '5px' }}>
{task.completed ? '未完了' : '完了 (PATCH)'}
</button>
<button onClick={() => handleDeleteTask(task.id)} style={{ backgroundColor: 'red', color: 'white' }}>
削除 (DELETE)
</button>
</li>
))}
</ul>
)}
</div>
);
}
export default App;
【App.jsx
のコード解説】
-
PATCHリクエストの処理 (
handleToggleCompleted
)-
URLの指定:
fetch(\
${API_URL}/${taskId}`)` のように、URLに更新したいタスクのIDを含めます。 -
ステートの更新:
setTasks(tasks.map(...))
を使って、既存のtasks
配列を新しい情報で置き換えています。-
map
内でIDが一致するタスクだけを、サーバーから返ってきたupdatedTask
に置き換えることで、画面を再読み込みせずに完了状態を切り替えています。
-
-
URLの指定:
-
DELETEリクエストの処理 (
handleDeleteTask
)-
method: 'DELETE'
: 削除要求であることをサーバーに伝えます。 -
response.status !== 204
: サーバーは成功時に204 No Content
を返します。コードではこれをチェックし、それ以外の場合はエラーとしています。 -
ステートの更新:
setTasks(tasks.filter(task => task.id !== taskId))
を使います。-
filter
関数は、削除対象のIDと一致しない要素だけで新しい配列を作成し、ステートを更新します。これにより、削除したタスクが画面から消えます。
-
-
3. まとめ:Webサービスの基本CRUD操作を完全制覇!
今回は、タスクの削除機能(DELETE)を実装し、Webサービスの基本機能をすべて網羅しました。
-
永続化:
fs
モジュールを使い、データをサーバーのメモリからJSONファイルに移動させました。 -
CRUD完全制覇: これで、Webサービスのすべての基本操作が揃いました。
- Create (POST)
- Read (GET)
- Update (PATCH)
- Delete (DELETE)
これで、あなたは「クライアント(React)からサーバー(Node.js)にリクエストを送り、データを操作し、永続化する」という、Webアプリケーションの全体像を完全に理解しました。
次回は、このタスクアプリの使い勝手を向上させるために、デザイン(CSS)の調整や、より高度なReactのテクニック(フォームの管理など)に挑戦しましょう!
今回もお疲れ様でした!次回も頑張ってこー!
おまけ
くすりの窓口では、現在インターンや中途採用を募集しています。
気になる方は以下のリンクよりご確認ください。