この記事ではNext.jsとLaravelを使ってCRUD機能がある簡単なWebアプリケーションを作る方法を解説していきます。
また、今回はただCRUD機能をつくるだけでなく、LaravelのSanctumを使ってCSRF対策も実装するので、それについても解説していきます。
開発環境
- macOS Venture 13.2.1
- Laravel 10.15.0
- Next.js 13.4.16
LaravelのAPIの動作確認はPostmanを使って行っており、画面のデザインはTailwind CSSを使って整えています。
また、今回のCRUD機能はstudentsという
- id
- name
- created_at
- updated_at
のカラムを持ったテーブルを使って、データの表示、新規登録、編集、削除ができるようなものを作成していきます。
LaravelのSanctumでCSRF対策を実装
Next.jsとLaravelのプロジェクトを新しく作成し、LaravelのSanctumを使ってSPA認証を実装していきます。
SanctumでSPA認証を実装することによってCSRF対策を行うことができます。どのように実装するのかは、こちらの記事で詳しく解説しているので参考にしてみてください。
上記の記事のように、Next.jsとLaravelのプロジェクトを作成してSanctumのSAP認証の実装も完了したら、実際にCRUD機能をつくっていきます。
Laravelでマイグレーションとシーダーを作成する
まずはLaravelの方からコードを書いていきます。ターミナルでLaravelのコンテナの中に入り、以下のコマンドを実行してモデルとマイグレーションを作成してください。
php artisan make:model Student --migration
作成したマイグレーションファイルの中身を以下のように編集します。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('students', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('students');
}
};
次は、studentsテーブルにデータを入れるためのシーダーを作成します。以下のコマンドを実行してシーダーを作成してください。
php artisan make:seeder StudentSeeder
シーダーが作成できたら、中身を以下のように書き換えます。
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class StudentSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$students = [
[ 'name' => '山田一郎' ],
[ 'name' => '山田二郎' ],
[ 'name' => '山田三郎' ],
[ 'name' => '山田四郎' ],
[ 'name' => '山田五郎' ],
];
foreach ($students as $student) {
DB::table('students')->insert([
'name' => $student['name'],
'created_at' => Carbon::now(),
'updated_at' => Carbon::now()
]);
}
}
}
また、DatabaseSeeder.phpで呼び出されるシーダーにStudentSeederを追加しておきます。
public function run(): void
{
// StudentSeederを実行するための記述
$this->call([
StudentSeeder::class,
]);
}
ここまでできたら、以下のコマンドでマイグレーションとシーダーを実行します。
php artisan migrate
php artisan db:seed
これでstudentsのテーブルとテストデータを作成することができます。ターミナルに以下のような表示が出ていればマイグレーションとシーダーの実行は完了です。
LaravelでCRUDの処理とルーティングを作成
次はLaravelのコントローラーにCRUDの処理を書いて、api.phpにそれらの処理のためのルーティングも書いていきます。
まずは、以下のコマンドでコントローラーを作成してください。
php artisan make:controller StudentController
作成したら、コントローラーの中身を以下のようにしてください。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Student;
class StudentController extends Controller
{
// 一覧表示
public function index() {
$students = Student::all();
return response()->json([
'data' => $students
], 200);
}
// 登録
public function store(Request $request) {
$student = new Student();
$student->name = $request->name;
$student->save();
return response()->json([
'data' => $student
], 201);
}
// 指定のデータのみ取得
public function edit($id) {
$student = Student::find($id);
return response()->json([
'data' => $student
], 200);
}
// 更新
public function update(Request $request, Student $student) {
$student->fill($request->all());
$student->save();
return response()->json([
'data' => $student
], 200);
}
// 削除
public function delete(Student $student) {
$student->delete();
return response()->json([
'message' => 'deleted successfully.'
], 200);
}
}
これらがCRUD機能を作るためにLaravel側で必要な処理ですね。
続いてはルーティングを設定していきます。routesディレクトリにあるapi.phpに以下の記述を追加してください。
// studentsのCRUDのルーティング
Route::get('/students', 'App\Http\Controllers\StudentController@index');
Route::post('/students', 'App\Http\Controllers\StudentController@store');
Route::get('/students/{student:id}', 'App\Http\Controllers\StudentController@edit');
Route::patch('/students/{student:id}', 'App\Http\Controllers\StudentController@update');
Route::delete('/students/{student:id}', 'App\Http\Controllers\StudentController@delete');
ここまでできたら、実際にLaravelのAPIでCRUDの機能ができているかどうかをPostmanを使って確認してみましょう。
Postmanを使ってstudentsテーブルのデータの表示、新規登録、編集、削除ができるかどうかが確認できたら、Laravelの方の実装は完了です。
Next.jsでデータ表示の機能を作る
次はNext.jsの方の実装をしていきます。まずはstudentテーブルのデータを表示する機能を作成していきますね。
Next.jsのappディレクトリの中に新しくstudentsというディレクトリを作成し、その中にpage.tsxというファイルを作成してください。
このpage.tsxの中身は以下のようにします。
"use client";
import React, { useEffect, useState } from 'react';
import axios from 'axios';
// Next.js13ではnext/routerではなくnext/navigationからuseRouterをインポートする
import { useRouter } from 'next/navigation';
// XSRF-TOKENをリクエスト時に送信するための設定
const http = axios.create({
baseURL: 'http://localhost:8080',
withCredentials: true,
});
const Students = () => {
// 生徒一覧を格納するstate
const [students, setStudents] = useState([]);
// router
const router = useRouter();
// LaravelのAPIにリクエストを送り、生徒一覧を取得する関数
const getStudents = async () => {
const response = await fetch('http://localhost:8080/api/students');
const json = await response.json();
setStudents(json.data);
}
// ページが読み込まれたら生徒一覧を取得する
useEffect(() => {
getStudents();
}, []);
// 生徒のデータを削除する処理
const deleteStudent = async (id: number) => {
// APIにリクエストを送信して、データを削除する
http.delete(`/api/students/${id}`).then(()=>{
// 完了したら、再度データを取得
getStudents();
});
}
return (
<div className="relative overflow-x-auto">
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead className="text-base text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-6 py-3">
ID
</th>
<th scope="col" className="px-6 py-3">
名前
</th>
<th scope="col" className="px-3 py-3 text-right">
<button
className="text-white bg-purple-700 hover:bg-purple-800 focus:outline-none focus:ring-4 focus:ring-purple-300 font-medium rounded-full text-sm px-5 py-2.5 text-center mb-2 dark:bg-purple-600 dark:hover:bg-purple-700 dark:focus:ring-purple-900"
onClick={()=>{
router.push('/students/create');
}}
>
新規登録
</button>
</th>
<th scope="col" className="px-3 py-3">
</th>
</tr>
</thead>
<tbody>
{
students.map((student: any)=>{
return (
<tr key={student.id} className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{student.id}
</th>
<td className="px-6 py-4 text-base">
{student.name}
</td>
<td className="px-3 py-4 text-right">
<button
className="text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 font-medium rounded-full text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={()=>{
}}
>
編集
</button>
</td>
<td className="px-3 py-4">
<button
className="text-white bg-gray-800 hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-full text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:border-gray-700"
onClick={()=>{
}}
>
削除
</button>
</td>
</tr>
)
})
}
</tbody>
</table>
</div>
);
}
export default Students;
getStudentsという関数でLaravelのAPIにリクエストを送り、studentsテーブルのデータを取得するLaravelのStudentコントローラーにあるindex関数を呼び出しています。
// 一覧表示
public function index() {
$students = Student::all();
return response()->json([
'data' => $students
], 200);
}
また、getStudentsはページがuseEffectを使ってレンダリングされたときに実行されるようになっており、レスポンスとして返ってきたデータはuseStateに格納しています。
実際に、このページにアクセスしてみると以下のようにstudentsテーブルのデータが画面に表示されているはずです。
Next.jsで新規登録の機能を作る
次は、生徒のデータを新規登録する機能を作成していきます。Next.jsのstudentsディレクトリの中に新しくcreateディレクトリを作成し、その中にpage.tsxを作成します。
このpage.tsxの中身は以下のようにしてください。
"use client";
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useRouter } from 'next/navigation';
// XSRF-TOKENをリクエスト時に送信するための設定
const http = axios.create({
baseURL: 'http://localhost:8080',
withCredentials: true,
});
const Page = () => {
// 生徒の名前を格納するstate
const [name, setName] = useState('');
// router
const router = useRouter();
// 生徒のデータを新しく追加する関数
const createStudent = async () => {
// 登録するデータ
const requestBody = {
name: name,
};
// APIにリクエストを送信して、データを登録する
http.post("/api/students", requestBody, {
headers: {
'Content-Type': 'application/json'
},
}).then(()=>{
// 登録が完了したら、生徒一覧ページに遷移する
router.push('/students');
});
}
return (
<div>
<div className="py-2 px-4">
<p>追加する生徒の名前を入力してください</p>
</div>
<input
type="text"
className='bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500 m-3 max-w-sm'
placeholder='name'
onChange={(e) => {
setName(e.target.value);
}}
/>
<div>
<button
className="shadow bg-purple-500 hover:bg-purple-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded m-3"
onClick={()=>{
createStudent();
}}
>送信</button>
</div>
</div>
);
}
export default Page;
createStudent関数がLaravelのAPIに新しく生徒のデータを作成するリクエストを送信する関数ですね。
入力欄に入力された文字列をonChangeとuseStateを使って受け取り、createStudent関数の中にあるrequestBodyのnameに渡しています。
また、新規登録ではLaravelのStudentコントローラーのstoreメソッドが実行されています。
// 登録
public function store(Request $request) {
$student = new Student();
$student->name = $request->name;
$student->save();
return response()->json([
'data' => $student
], 201);
}
実際の画面を開くと以下のような入力欄ができていると思います。
生徒の名前を入力して、先ほどの生徒一覧に入力した名前が追加されるか確かめてみましょう。
CSRF対策ができていることを確認する
POSTメソッドでデータを新規作成する機能がちゃんとできていることが確認できたので、CSRF対策ができているかどうかもここで確認しておきましょう。
先ほどstudents/createディレクトリに作成したpage.tsxのcreateStudent関数の中身を以下のように変更してください。
// 生徒のデータを新しく追加する関数
const createStudent = async () => {
// 登録するデータ
const requestBody = {
name: name,
};
// CSRF対策のトークンを含まずにリクエストを送る
await fetch("http://localhost:8080/api/students", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody),
credentials: 'include',
});
}
createStudent関数を上記のように変更して、実際に生徒の新規登録を行うと以下のように419のエラーが出ることが確認できるはずです。
先ほど作成した新規登録の関数と違い、上記の関数はCSRFトークンを含めずにPOSTのリクエストを送っているため、419のエラーが起こっています。
つまり、Laravel Sanctumを使ったCSRF対策は機能しているということですね。
※CSRF対策ができていることが確認できたら、createStudent関数の中身は元に戻しておいてください。
Next.jsでデータの編集機能を作る
次は、データの編集画面とデータを編集する機能を作成していきます。データの編集画面はstudentsテーブルのデータのidがURLに含まれるようにしていきます。
例えば、idが2のデータを編集する画面のURLは
/students/edit/2
になるという感じですね。こちらはNext.jsの動的ルーティングを使って実装します。
Next.jsのappディレクトリの中のstudentsディレクトリに、editというディレクトリを作成し、その中に[id]というディレクトリを作成します。その中にpage.tsxというファイルを作成してください。
これで編集ページのURLに編集するstudentsテーブルのデータのidが含まれるようになります。
編集ページのpage.tsxの中身は以下のようにしてください。
"use client";
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useRouter } from 'next/navigation';
// XSRF-TOKENをリクエスト時に送信するための設定
const http = axios.create({
baseURL: 'http://localhost:8080',
withCredentials: true,
});
// 動的ルーティングのパスを取得するためにparamsを引数として受け取る
const Page = ({ params }: { params: { id: string } }) => {
// 生徒のデータを格納するstate
const [student, setStudent] = useState<any>({});
// router
const router = useRouter();
// LaravelのAPIにリクエストを送り、生徒のデータを取得する関数
const getStudent = async () => {
const response = await fetch(`http://localhost:8080/api/students/${params.id}`);
const json = await response.json();
setStudent(json.data);
}
// ページが読み込まれたら生徒のデータを取得する
useEffect(() => {
getStudent();
}, []);
// 生徒の名前を更新する関数
const updateStudent = async () => {
// 更新するデータ
const requestBody = {
name: student.name,
};
// APIにリクエストを送信して、データを登録する
http.patch(`/api/students/${student.id}`, requestBody, {
headers: {
'Content-Type': 'application/json'
},
}).then(()=>{
// 登録が完了したら、生徒一覧ページに遷移する
router.push('/students');
});
}
return (
<div>
<div className="py-2 px-4">
<p>IDが{params.id}番の生徒の名前を入力してください</p>
</div>
<input
type="text"
className='bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500 m-3 max-w-sm'
placeholder='name'
defaultValue={student.name}
onChange={(e) => {
setStudent({
...student,
name: e.target.value
});
}}
/>
<div>
<button
className="shadow bg-purple-500 hover:bg-purple-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded m-3"
onClick={()=>{
updateStudent();
}}
>送信</button>
</div>
</div>
);
}
export default Page;
動的ルーティングのパラメータになっているidをURLから受け取り、そのidを持つデータを取得するためのリクエストをLaravelのAPIに送り、編集対象の生徒の名前をページがレンダリングされたときに入力欄に入れておきます。
このときにはLaravelのStudentコントローラーのeditメソッドが呼び出されています。
// 指定のデータのみ取得
public function edit($id) {
$student = Student::find($id);
return response()->json([
'data' => $student
], 200);
}
また、編集ボタンが押されるとLaravelのStudentコントローラーにあるupdateメソッドにリクエストが送られるようになっています。
// 更新
public function update(Request $request, Student $student) {
$student->fill($request->all());
$student->save();
return response()->json([
'data' => $student
], 200);
}
ここまでできたら、生徒のデータを一覧表示するページに、編集ページのリンクを入れていきます。
studentsディレクトリにあるpage.tsxの編集ボタンの部分を以下のように編集してください。
<button
className="text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 font-medium rounded-full text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={()=>{
router.push(`/students/edit/${student.id}`);
}}
>
編集
</button>
一覧表示の画面を開いて、編集ボタンをクリックすると、そのデータの編集ページに遷移して、実際にデータの編集ができるようになっていることが確認できるはずです。
Next.jsでデータの削除機能を作る
最後にデータを削除する機能を作成していきます。Next.jsのstudentsディレクトリにあるpage.tsxに以下の関数を追記してください。
// 生徒のデータを削除する処理
const deleteStudent = async (id: number) => {
// APIにリクエストを送信して、データを削除する
http.delete(`/api/students/${id}`).then(()=>{
// 完了したら、再度データを取得
getStudents();
});
}
こちらの関数がLaravelのAPIにデータを削除するリクエストを送る関数ですね。また、削除ボタンが押されたときにこの関数が実行されるようにしておきます。
<button
className="text-white bg-gray-800 hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-full text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:border-gray-700"
onClick={()=>{
deleteStudent(student.id);
}}
>
削除
</button>
削除のリクエストはLaravelのStudentコントローラーのdeleteメソッドに送られ、データの削除が実行されるようになっています。
// 削除
public function delete(Student $student) {
$student->delete();
return response()->json([
'message' => 'deleted successfully.'
], 200);
}
ここまでできたら、実際に削除ボタンを押してデータを削除する関数が問題なく実行されるかどうかを確認してみましょう。
バリデーションとエラーメッセージの表示
次はLaravelのAPIにバリデーションをつけて、Next.jsの画面にエラーメッセージが表示されるようにしていきます。
今回は、生徒の新規登録にバリデーションを機能をつけます。
まずは、Laravelの方にバリデーション機能をつけるためのリクエストを作成します。ターミナルでLaravelのコンテナの中に入り、以下のコマンドを入力してください。
php artisan make:request StudentRequest
これでRequestディレクトリにStudentRequest.phpというファイルが作成されるので、中身を以下のように変更してください。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StudentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'name' => 'required|max:225',
];
}
public function attributes()
{
return [
'name' => '生徒名',
];
}
public function messages()
{
return [
'name.required' => ':attributeを入力してください。',
'name.max' => ':attributeは:max文字以内で入力してください。',
];
}
}
次はStudentController.phpを以下のように編集してください。
// 追記
use App\Http\Requests\StudentRequest;
// RequestをStudentRequestに変更
public function store(StudentRequest $request) {
$student = new Student();
$student->name = $request->name;
$student->save();
return response()->json([
'data' => $student
], 201);
}
先ほど作成したStudentRequestをコントローラー内で使うための記述を追加して、storeメソッドの引数にあったRequestをStudentRequestに変更しています。
これでnameフィールドが必須項目になり、文字数も255文字以下でなければ入力できなくなります。
Laravelの方はこれでOKです。
次はNext.jsの方でバリデーションのエラーメッセージが画面に表示されるようにしていきます。
Next.jsのプロジェクトのapp/students/create/page.tsxの中身を以下のように編集してください。
"use client";
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { AxiosError } from 'axios';
import { useRouter } from 'next/navigation';
// XSRF-TOKENをリクエスト時に送信するための設定
const http = axios.create({
baseURL: 'http://localhost:8080',
withCredentials: true,
});
const Page = () => {
// 生徒の名前を格納するstate
const [name, setName] = useState('');
// エラーメッセージを格納するstate
const [errorMessage, setErrorMessage] = useState('');
// router
const router = useRouter();
// 生徒のデータを新しく追加する関数
const createStudent = async () => {
// 登録するデータ
const requestBody = {
name: name,
};
// APIにリクエストを送信して、データを登録する
http.post("/api/students", requestBody, {
headers: {
'Content-Type': 'application/json'
},
}).then(() => {
// 登録が完了したら、生徒一覧ページに遷移する
router.push('/students');
}).catch((err: AxiosError) => {
// err.response.data.messageがエラーメッセージ
setErrorMessage(err.response.data.message);
});
}
return (
<div>
<div className="py-2 px-4">
<p>追加する生徒の名前を入力してください</p>
</div>
<input
type="text"
className='bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500 m-3 max-w-sm'
placeholder='name'
onChange={(e) => {
setName(e.target.value);
}}
/>
<div className="py-1 px-4">
<p className="text-red-500">{errorMessage}</p>
</div>
<div>
<button
className="shadow bg-purple-500 hover:bg-purple-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded m-3"
onClick={()=>{
createStudent();
}}
>送信</button>
</div>
</div>
);
}
export default Page;
どの部分を変更したのか詳しく解説します。
まずは、バリデーションではじかれたときのエラーを取得するために、AxiosErrorをインポートします。
import { AxiosError } from 'axios';
次に、エラーメッセージを格納するためのuseStateを定義します。
// エラーメッセージを格納するstate
const [errorMessage, setErrorMessage] = useState('');
createStudent関数を以下のように変更します。
// 生徒のデータを新しく追加する関数
const createStudent = async () => {
// 登録するデータ
const requestBody = {
name: name,
};
// APIにリクエストを送信して、データを登録する
http.post("/api/students", requestBody, {
headers: {
'Content-Type': 'application/json'
},
}).then(() => {
// 登録が完了したら、生徒一覧ページに遷移する
router.push('/students');
}).catch((err: AxiosError) => {
// err.response.data.messageがエラーメッセージ
setErrorMessage(err.response.data.message);
});
}
そして、実際にエラーメッセージを表示されるための記述も追記しておきます。
<div className="py-1 px-4">
<p className="text-red-500">{errorMessage}</p>
</div>
Next.jsの方もこれで完了です。
実際に、生徒の新規登録画面から生徒名を入力せずに送信を押したり、文字数を255文字より多くして送信を押したりすると、エラーメッセージが表示されることが確認できるはずです。
まとめ
Next.jsとLaravelを使ってCRUD機能を作成することができる。また、LaravelのAPIのCSRF対策はLaravel SanctumのSPA認証を使って実装することができる。
今回の実装で使ったLaravelとNext.jsのプロジェクトのリポジトリはこちらです。
- Next.js
- Laravel