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?

第5回:【React &Node.js】CRUDでWebサービスの基礎を完成させる

Last updated at Posted at 2025-10-10

みなさん、こんにちは!

前回、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**ステータスを返します。

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に置き換えることで、画面を再読み込みせずに完了状態を切り替えています。
  • 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のテクニック(フォームの管理など)に挑戦しましょう!
今回もお疲れ様でした!次回も頑張ってこー!

おまけ

くすりの窓口では、現在インターンや中途採用を募集しています。
気になる方は以下のリンクよりご確認ください。

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?