LaravelのAPIをReactでフロントエンド実装

はじめに

モダンなウェブアプリケーション開発において、バックエンドとフロントエンドを分離する構成が主流となっています。特に、LaravelでRESTful APIを構築し、Reactでフロントエンドを実装する組み合わせは、柔軟性と拡張性に優れた開発手法として注目されています。

この記事では、LaravelでAPIを作成し、ReactでそのAPIを消費する実装方法について解説します。バックエンドとフロントエンドの分離により、開発の効率化やメンテナンス性の向上が期待できます。

開発環境の準備

バックエンド(Laravel)

まず、Laravelプロジェクトを作成します。Composerを使用してインストールしましょう。

composer create-project laravel/laravel laravel-api
cd laravel-api

フロントエンド(React)

次に、Reactプロジェクトを作成します。create-react-appを使ってセットアップします。

npx create-react-app react-frontend
cd react-frontend

LaravelでAPIを構築する

1. データベースの設定

.envファイルを編集して、データベース接続情報を設定します。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_api
DB_USERNAME=root
DB_PASSWORD=

2. モデルとマイグレーションの作成

例として、タスク管理アプリを想定し、Taskモデルを作成します。

php artisan make:model Task -m

マイグレーションファイルを編集します。

// database/migrations/xxxx_xx_xx_create_tasks_table.php
public function up()
{
    Schema::create('tasks', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('description')->nullable();
        $table->boolean('completed')->default(false);
        $table->timestamps();
    });
}

マイグレーションを実行します。

php artisan migrate

3. コントローラーの作成

TaskControllerを作成します。

php artisan make:controller TaskController --api

コントローラーを編集します。

// app/Http/Controllers/TaskController.php
<?php

namespace App\Http\Controllers;

use App\Models\Task;
use Illuminate\Http\Request;

class TaskController extends Controller
{
    /**
     * タスク一覧を取得
     */
    public function index()
    {
        $tasks = Task::all();
        return response()->json($tasks);
    }

    /**
     * 新しいタスクを保存
     */
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'completed' => 'boolean',
        ]);

        $task = Task::create($validated);
        return response()->json($task, 201);
    }

    /**
     * 特定のタスクを表示
     */
    public function show($id)
    {
        $task = Task::findOrFail($id);
        return response()->json($task);
    }

    /**
     * 特定のタスクを更新
     */
    public function update(Request $request, $id)
    {
        $task = Task::findOrFail($id);
        
        $validated = $request->validate([
            'title' => 'string|max:255',
            'description' => 'nullable|string',
            'completed' => 'boolean',
        ]);

        $task->update($validated);
        return response()->json($task);
    }

    /**
     * 特定のタスクを削除
     */
    public function destroy($id)
    {
        $task = Task::findOrFail($id);
        $task->delete();
        return response()->json(null, 204);
    }
}

4. モデルの更新

Taskモデルを編集して、mass assignmentを許可します。

// app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'description', 'completed'];
}

5. APIルートの設定

api.phpファイルにルートを追加します。

// routes/api.php
use App\Http\Controllers\TaskController;

Route::apiResource('tasks', TaskController::class);

6. CORSの設定

フロントエンドからAPIにアクセスするために、CORSを設定します。

php artisan cors:install

config/cors.phpファイルを編集します。

return [
    'paths' => ['api/*'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['http://localhost:3000'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => false,
];

Reactでフロントエンドを実装する

1. 必要なパッケージのインストール

npm install axios react-router-dom

2. APIサービスの作成

APIとの通信を行うサービスを作成します。

// src/services/api.js
import axios from 'axios';

const API_URL = 'http://localhost:8000/api';

export const fetchTasks = async () => {
  try {
    const response = await axios.get(`${API_URL}/tasks`);
    return response.data;
  } catch (error) {
    console.error('タスク取得エラー:', error);
    throw error;
  }
};

export const createTask = async (taskData) => {
  try {
    const response = await axios.post(`${API_URL}/tasks`, taskData);
    return response.data;
  } catch (error) {
    console.error('タスク作成エラー:', error);
    throw error;
  }
};

export const updateTask = async (id, taskData) => {
  try {
    const response = await axios.put(`${API_URL}/tasks/${id}`, taskData);
    return response.data;
  } catch (error) {
    console.error('タスク更新エラー:', error);
    throw error;
  }
};

export const deleteTask = async (id) => {
  try {
    await axios.delete(`${API_URL}/tasks/${id}`);
    return true;
  } catch (error) {
    console.error('タスク削除エラー:', error);
    throw error;
  }
};

3. コンポーネントの作成

タスク一覧コンポーネント

// src/components/TaskList.jsx
import React, { useState, useEffect } from 'react';
import { fetchTasks, deleteTask } from '../services/api';
import TaskForm from './TaskForm';

const TaskList = () => {
  const [tasks, setTasks] = useState([]);
  const [loading, setLoading] = useState(true);
  const [editingTaskId, setEditingTaskId] = useState(null);

  useEffect(() => {
    loadTasks();
  }, []);

  const loadTasks = async () => {
    try {
      setLoading(true);
      const data = await fetchTasks();
      setTasks(data);
    } catch (error) {
      console.error('タスク読み込みエラー:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleDelete = async (id) => {
    if (window.confirm('このタスクを削除してもよろしいですか?')) {
      try {
        await deleteTask(id);
        setTasks(tasks.filter(task => task.id !== id));
      } catch (error) {
        console.error('削除エラー:', error);
      }
    }
  };

  const handleEdit = (id) => {
    setEditingTaskId(id);
  };

  const handleTaskUpdated = () => {
    setEditingTaskId(null);
    loadTasks();
  };

  if (loading) {
    return <div>読み込み中...</div>;
  }

  return (
    <div className="task-list">
      <h2>タスク一覧</h2>
      <TaskForm onTaskAdded={loadTasks} />
      
      {tasks.length === 0 ? (
        <p>タスクはありません。</p>
      ) : (
        <ul>
          {tasks.map(task => (
            <li key={task.id} className={task.completed ? 'completed' : ''}>
              {editingTaskId === task.id ? (
                <TaskForm 
                  task={task} 
                  isEditing={true} 
                  onTaskUpdated={handleTaskUpdated} 
                  onCancel={() => setEditingTaskId(null)}
                />
              ) : (
                <div>
                  <h3>{task.title}</h3>
                  <p>{task.description}</p>
                  <p>ステータス: {task.completed ? '完了' : '未完了'}</p>
                  <button onClick={() => handleEdit(task.id)}>編集</button>
                  <button onClick={() => handleDelete(task.id)}>削除</button>
                </div>
              )}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

export default TaskList;

タスクフォームコンポーネント

// src/components/TaskForm.jsx
import React, { useState, useEffect } from 'react';
import { createTask, updateTask } from '../services/api';

const TaskForm = ({ task, isEditing, onTaskAdded, onTaskUpdated, onCancel }) => {
  const [formData, setFormData] = useState({
    title: '',
    description: '',
    completed: false
  });

  useEffect(() => {
    if (task) {
      setFormData({
        title: task.title,
        description: task.description || '',
        completed: task.completed
      });
    }
  }, [task]);

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData({
      ...formData,
      [name]: type === 'checkbox' ? checked : value
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      if (isEditing && task) {
        await updateTask(task.id, formData);
        if (onTaskUpdated) onTaskUpdated();
      } else {
        await createTask(formData);
        setFormData({ title: '', description: '', completed: false });
        if (onTaskAdded) onTaskAdded();
      }
    } catch (error) {
      console.error('保存エラー:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="task-form">
      <h3>{isEditing ? 'タスクを編集' : '新しいタスクを追加'}</h3>
      <div>
        <label>
          タイトル:
          <input
            type="text"
            name="title"
            value={formData.title}
            onChange={handleChange}
            required
          />
        </label>
      </div>
      <div>
        <label>
          説明:
          <textarea
            name="description"
            value={formData.description}
            onChange={handleChange}
          />
        </label>
      </div>
      <div>
        <label>
          完了:
          <input
            type="checkbox"
            name="completed"
            checked={formData.completed}
            onChange={handleChange}
          />
        </label>
      </div>
      <button type="submit">{isEditing ? '更新' : '追加'}</button>
      {isEditing && <button type="button" onClick={onCancel}>キャンセル</button>}
    </form>
  );
};

export default TaskForm;

4. アプリケーションの構成

// src/App.js
import React from 'react';
import './App.css';
import TaskList from './components/TaskList';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>タスク管理アプリ</h1>
      </header>
      <main>
        <TaskList />
      </main>
    </div>
  );
}

export default App;

5. スタイルの追加

/* src/App.css */
.App {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.task-list ul {
  list-style: none;
  padding: 0;
}

.task-list li {
  border: 1px solid #ddd;
  margin-bottom: 10px;
  padding: 15px;
  border-radius: 5px;
}

.task-list li.completed {
  background-color: #f8f8f8;
  color: #666;
}

.task-form {
  margin-bottom: 20px;
  padding: 15px;
  border: 1px solid #eee;
  border-radius: 5px;
}

.task-form div {
  margin-bottom: 10px;
}

.task-form input[type="text"],
.task-form textarea {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

button {
  background-color: #4CAF50;
  border: none;
  color: white;
  padding: 8px 12px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 14px;
  margin-right: 5px;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #45a049;
}

アプリケーションの実行

バックエンド(Laravel)の起動

cd laravel-api
php artisan serve

これでLaravelのAPIサーバーが http://localhost:8000 で起動します。

フロントエンド(React)の起動

cd react-frontend
npm start

Reactアプリケーションが http://localhost:3000 で起動します。

まとめ

この記事では、LaravelでRESTful APIを構築し、Reactでフロントエンドを実装する方法について解説しました。この構成の主なメリットは次のとおりです:

  1. 関心の分離: バックエンドとフロントエンドの責務が明確に分かれ、メンテナンス性が向上します。
  2. スケーラビリティ: APIを構築することで、将来的に別のフロントエンド(モバイルアプリなど)の追加が容易になります。
  3. 開発効率: フロントエンドチームとバックエンドチームが独立して開発できます。
  4. ユーザーエクスペリエンス: Reactの仮想DOMにより、高速で応答性の高いUIを実現できます。

バックエンドとフロントエンドを分離することで、各技術の利点を最大限に活かした開発が可能になります。Laravelの強力なORM、認証機能、バリデーションなどと、Reactのコンポーネント指向の開発、状態管理、優れたパフォーマンスを組み合わせることで、より良いWebアプリケーションを構築できるでしょう。

次のステップ

  • 認証機能の追加(Laravel Sanctum)
  • 状態管理の改善(React ContextやReduxの導入)
  • ユニットテストとE2Eテストの実装
  • 本番環境へのデプロイ

これらの機能を追加することで、より堅牢でセキュアなアプリケーションを構築できます。

コメント

タイトルとURLをコピーしました