Laravel で実装した API と Next.js の連携に挑戦する
普段 PHP + CodeIgniter + jQuery + Bootstrap の構成で業務システムを開発している者が
Laravel + Next.js を触ってみた記録である。
動作確認環境
- Windows 10 Enterprise 22H2
技術構成
- PHP v8.2.12
- Laravel v11.11.0
- Next.js 14.2.4
ゴール設定
Laravel で実装した API を Next.js から叩く。
Laravel
インストール
> composer create-project laravel/laravel laravel-app
プロジェクト作成時に SQLite の DB ファイルが作成され、マイグレーションも実行される。
> cd .\laravel-app\
laravel-app> php artisan serve
ビルトインサーバーを起動して http://127.0.0.1:8000/ の表示確認をする。
タイムゾーンと言語設定
- APP_TIMEZONE=UTC
+ APP_TIMEZONE=Asia/Tokyo
- APP_LOCALE=en
- APP_FALLBACK_LOCALE=en
- APP_FAKER_LOCALE=en_US
+ APP_LOCALE=ja
+ APP_FALLBACK_LOCALE=ja
+ APP_FAKER_LOCALE=ja_JP
Laravel Breeze
いきなり横道にそれるが、Laravel Breeze を試してみる。
https://laravel.com/docs/11.x/starter-kits
Composer で Laravel Breeze パッケージを追加する。
※インストーラーなので開発環境だけ
laravel-app> composer require laravel/breeze --dev
インストールする。
laravel-app> php artisan breeze:install
Which Breeze stack would you like to install?
Blade with Alpine ............................................... blade
Livewire (Volt Class API) with Alpine ........................ livewire
Livewire (Volt Functional API) with Alpine ........ livewire-functional
React with Inertia .............................................. react
Vue with Inertia .................................................. vue
API only .......................................................... api
❯ blade
Would you like dark mode support? (yes/no) [no]
❯ no
Which testing framework do you prefer? [PHPUnit]
Pest ................................................................ 0
PHPUnit ............................................................. 1
❯ 1
使用するスタック、モダンフロントエンド何もワカランなので安牌の blade を選択した。
インストール前後でルーティングは以下のように変化する。
laravel-app> php artisan route:list
GET|HEAD / ..........................................................................................................
GET|HEAD up .........................................................................................................
Showing [2] routes
laravel-app> php artisan route:list
GET|HEAD / ..........................................................................................................
GET|HEAD confirm-password ................................ password.confirm › Auth\ConfirmablePasswordController@show
POST confirm-password .................................................. Auth\ConfirmablePasswordController@store
GET|HEAD dashboard ........................................................................................ dashboard
POST email/verification-notification ..... verification.send › Auth\EmailVerificationNotificationController@store
GET|HEAD forgot-password ................................. password.request › Auth\PasswordResetLinkController@create
POST forgot-password .................................... password.email › Auth\PasswordResetLinkController@store
GET|HEAD login ................................................... login › Auth\AuthenticatedSessionController@create
POST login ............................................................ Auth\AuthenticatedSessionController@store
POST logout ................................................ logout › Auth\AuthenticatedSessionController@destroy
PUT password .................................................. password.update › Auth\PasswordController@update
GET|HEAD profile .............................................................. profile.edit › ProfileController@edit
PATCH profile .......................................................... profile.update › ProfileController@update
DELETE profile ........................................................ profile.destroy › ProfileController@destroy
GET|HEAD register ................................................... register › Auth\RegisteredUserController@create
POST register ............................................................... Auth\RegisteredUserController@store
POST reset-password ........................................... password.store › Auth\NewPasswordController@store
GET|HEAD reset-password/{token} .................................. password.reset › Auth\NewPasswordController@create
GET|HEAD up .........................................................................................................
GET|HEAD verify-email .................................. verification.notice › Auth\EmailVerificationPromptController
GET|HEAD verify-email/{id}/{hash} .................................. verification.verify › Auth\VerifyEmailController
Showing [21] routes
この後、公式等で DB マイグレーションを実行する手順となっていたが必要なさそ?
ビルトインサーバーを起動して http://127.0.0.1:8000/ の表示確認をする。
右上に Log in と Register のリンクが追加されている。
以下の機能が利用できるようになった。なんだこれは、たまげたなあ。
- ユーザー登録
- ログイン
- パスワードリマインダ
- ダッシュボード
- プロフィール(ユーザー変更、ユーザー削除)
- ログアウト
日本語化
https://github.com/askdkc/breezejp
Composer で Laravel Breeze 日本語化パッケージを追加する。
※インストーラーなので開発環境だけ
laravel-app> composer require askdkc/breezejp --dev
インストールする。
laravel-app> php artisan breezejp
これで Laravel Breeze で追加された画面と Laravel のバリデーションメッセージが日本語化された。
API
Next.js から叩くためのユーザー API を実装してみる。
laravel-app> php artisan install:api
...
One new database migration has been published. Would you like to run all pending database migrations? (yes/no) [yes]:
> yes
新たに作成された routes/api.php
に以下のルーティングが追加された。
GET|HEAD api/user ...................................................................................................
GET|HEAD sanctum/csrf-cookie ...................... sanctum.csrf-cookie › Laravel\Sanctum › CsrfCookieController@show
sanctum(聖域)は何だかよくわからんが凄そうだ。
API 用の Controller を作成する。
laravel-app> php artisan make:controller UserController --resource
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
+ Route::apiResource('users', \App\Http\Controllers\UserController::class);
GET|HEAD api/users ......................................................... users.index › UserController@index
POST api/users ......................................................... users.store › UserController@store
GET|HEAD api/users/{user} .................................................... users.show › UserController@show
PUT|PATCH api/users/{user} ................................................ users.update › UserController@update
DELETE api/users/{user} .............................................. users.destroy › UserController@destroy
<?php
namespace App\Http\Controllers;
+ use App\Models\User;
+ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*/
- public function index()
+ public function index(): JsonResponse
{
- //
+ $users = User::all();
+ return response()->json(['users' => $users]);
}
-
- /**
- * Show the form for creating a new resource.
- */
- public function create()
- {
- //
- }
/**
* Store a newly created resource in storage.
*/
- public function store(Request $request)
+ public function store(Request $request): JsonResponse
{
- //
+ $user = User::create([
+ 'name' => $request->get('name'),
+ 'email' => $request->get('email'),
+ 'password' => $request->get('password'),
+ ]);
+ return response()->json(['user' => $user]);
}
/**
* Display the specified resource.
*/
- public function show(string $id)
+ public function show(string $id): JsonResponse
{
- //
+ $user = User::find($id);
+ return response()->json(['user' => $user]);
}
-
- /**
- * Show the form for editing the specified resource.
- */
- public function edit(string $id)
- {
- //
- }
/**
* Update the specified resource in storage.
*/
- public function update(Request $request, string $id)
+ public function update(Request $request, string $id): JsonResponse
{
- //
+ $user = User::findOrFail($id);
+ $user->update([
+ 'name' => $request->get('name'),
+ 'email' => $request->get('email'),
+ 'password' => $request->get('password'),
+ ]);
+ return response()->json(['user' => $user]);
}
/**
* Remove the specified resource from storage.
*/
- public function destroy(string $id)
+ public function destroy(string $id): JsonResponse
{
- //
+ $user = User::findOrFail($id);
+ $user->delete();
+ return response()->json(null, 204);
}
}
ひと通り実装できたので試運転をしてみる。
index
リクエスト
GET http://127.0.0.1:8000/api/users
Accept: application/json
レスポンス
HTTP/1.1 200 OK
データがない場合
{
"users": []
}
データがある場合
{
"users": [
{
"id": 1,
"name": "Test1",
"email": "test1@example.com",
"email_verified_at": null,
"created_at": "2024-06-20T02:36:20.000000Z",
"updated_at": "2024-06-20T02:36:20.000000Z"
},
{
"id": 2,
"name": "Test2",
"email": "test2@example.com",
"email_verified_at": null,
"created_at": "2024-06-20T05:33:51.000000Z",
"updated_at": "2024-06-20T05:33:51.000000Z"
}
]
}
store
リクエスト
POST http://127.0.0.1:8000/api/users
Content-Type: application/json
{
"name": "Test3",
"email": "test3@example.com",
"password": "password"
}
レスポンス
HTTP/1.1 200 OK
{
"user": {
"name": "Test3",
"email": "test3@example.com",
"updated_at": "2024-06-20T05:37:06.000000Z",
"created_at": "2024-06-20T05:37:06.000000Z",
"id": 3
}
}
show
リクエスト
GET http://127.0.0.1:8000/api/users/3
Accept: application/json
レスポンス
HTTP/1.1 200 OK
{
"user": {
"id": 3,
"name": "Test3",
"email": "test3@example.com",
"email_verified_at": null,
"created_at": "2024-06-20T05:37:06.000000Z",
"updated_at": "2024-06-20T05:37:06.000000Z"
}
}
update
リクエスト
PUT http://127.0.0.1:8000/api/users/3
Content-Type: application/json
{
"name": "Test4",
"email": "test4@example.com",
"password": "password"
}
レスポンス
HTTP/1.1 200 OK
{
"user": {
"id": 3,
"name": "Test4",
"email": "test4@example.com",
"email_verified_at": null,
"created_at": "2024-06-20T05:37:06.000000Z",
"updated_at": "2024-06-20T05:41:52.000000Z"
}
}
destroy
リクエスト
DELETE http://127.0.0.1:8000/api/users/2
Accept: application/json
レスポンス
HTTP/1.1 204 No Content
<Response body is empty>
Response code: 204 (No Content); Time: 248ms (248 ms); Content length: 0 bytes (0 B)
バリデーション
新規登録 API にバリデーションを追加する為、リクエストクラスを作成する。
laravel-app> php artisan make:request UserStoreRequest
<?php
namespace App\Http\Requests;
+ use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
+ use Illuminate\Http\Exceptions\HttpResponseException;
class UserStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
- return false;
+ return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
- //
+ 'name' => [
+ 'required',
+ 'string',
+ 'max:255',
+ ],
+ 'email' => [
+ 'required',
+ 'string',
+ 'email',
+ 'unique:users,email',
+ ],
+ 'password' => [
+ 'required',
+ 'string',
+ 'min:8',
+ ],
];
}
+
+ /**
+ * @param Validator $validator
+ */
+ protected function failedValidation(Validator $validator)
+ {
+ $data = [
+ 'errors' => $validator->errors()->toArray(),
+ ];
+ throw new HttpResponseException(response()->json($data, 422));
+ }
}
今回は API リクエストに対する認証は不要にする為、authorize メソッドを変更する。
バリデーションルールを rules メソッドに定義する。
バリデーション失敗時に HTML ではなく JSON を返却したいので failedValidation メソッドを生やす。
<?php
namespace App\Http\Controllers;
+ use App\Http\Requests\UserStoreRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UserController extends Controller
{
//
- public function store(Request $request): JsonResponse
+ public function store(UserStoreRequest $request): JsonResponse
{
//
}
//
}
作成したリクエストクラスを使用するようにコントローラーを変更する。
続けて更新 API にバリデーションを追加していく。
各リクエストクラスに毎回 failedValidation メソッドを生やすのが面倒なので FormRequest を継承した API 用のベースクラスを作成する。
laravel-app> php artisan make:request ApiRequest
<?php
namespace App\Http\Requests;
+ use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
+ use Illuminate\Http\Exceptions\HttpResponseException;
class ApiRequest extends FormRequest
{
- /**
- * Determine if the user is authorized to make this request.
- */
- public function authorize(): bool
- {
- return false;
- }
-
- /**
- * Get the validation rules that apply to the request.
- *
- * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
- */
- public function rules(): array
- {
- return [
- //
- ];
- }
-
+ /**
+ * @param Validator $validator
+ */
+ protected function failedValidation(Validator $validator)
+ {
+ $data = [
+ 'errors' => $validator->errors()->toArray(),
+ ];
+ throw new HttpResponseException(response()->json($data, 422));
+ }
}
更新 API にバリデーションを追加する為、リクエストクラスを作成する。
laravel-app> php artisan make:request UserUpdateRequest
<?php
namespace App\Http\Requests;
- use Illuminate\Foundation\Http\FormRequest;
+ use Illuminate\Validation\Rule;
- class UserUpdateRequest extends FormRequest
+ class UserUpdateRequest extends ApiRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
- return false;
+ return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
- //
+ 'name' => [
+ 'string',
+ 'max:255',
+ ],
+ 'email' => [
+ 'string',
+ 'email',
+ Rule::unique('users')->ignore($this->user),
+ ],
+ 'password' => [
+ 'string',
+ 'min:8',
+ ],
];
}
}
ignore メソッドへの指定、コントローラーでは $id で受け取っているのでそれを指定すればいいけど、リクエストでどのように書けばいいのか謎だった。
https://eza-s.com/blog/archives/73/ を参考にさせていただいた。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UserStoreRequest;
+ use App\Http\Requests\UserUpdateRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
- use Illuminate\Http\Request;
class UserController extends Controller
{
//
- public function update(Request $request, string $id): JsonResponse
+ public function update(UserUpdateRequest $request, string $id): JsonResponse
{
//
}
//
}
新規登録 API 用のリクエストクラスも API 用のベースクラスを使用するように変更する。
<?php
namespace App\Http\Requests;
- use Illuminate\Contracts\Validation\Validator;
- use Illuminate\Foundation\Http\FormRequest;
- use Illuminate\Http\Exceptions\HttpResponseException;
-
- class UserStoreRequest extends FormRequest
+ class UserStoreRequest extends ApiRequest
{
//
-
- /**
- * @param Validator $validator
- */
- protected function failedValidation(Validator $validator)
- {
- $data = [
- 'errors' => $validator->errors()->toArray(),
- ];
- throw new HttpResponseException(response()->json($data, 422));
- }
}
バリデーションの試運転をしてみる。
store
リクエスト
POST http://127.0.0.1:8000/api/users
Content-Type: application/json
{
"name": "Test4",
"email": "test4@example.com",
"password": "pass"
}
レスポンス
HTTP/1.1 422 Unprocessable Content
{
"errors": {
"email": [
"メールアドレスの値は既に存在しています。"
],
"password": [
"パスワードは、8文字以上で指定してください。"
]
}
}
update
リクエスト
PUT http://127.0.0.1:8000/api/users/3
Content-Type: application/json
{
"name": "",
"email": "test4@example.com",
"password": "pass"
}
レスポンス
HTTP/1.1 422 Unprocessable Content
{
"errors": {
"name": [
"名前は文字列を指定してください。"
],
"password": [
"パスワードは、8文字以上で指定してください。"
]
}
}
Next.js
TypeScript - React - Next.js を利用する為には Node.js が必要。
Node.js の環境構築をする際によく出てくるマネージャーたち を片手に親子関係にテンパらないように進める。
ここでのターミナル操作は Windows PowerShell で進める。
Windows Package Manager(winget)
まずはパッケージマネージャー、こいつが無ければ始まらない。
> winget
v1.7.11261 の Windows パッケージ マネージャー
Copyright (c) Microsoft Corporation. All rights reserved.
最新版がインストール済みだったのでそのまま進む。
nvm for Windows
Node.js の バージョンを管理するツールは色々ある。
せっかくだから俺はこの nvm for Windows を選ぶぜ。
winget を使ってインストールする。
> winget install CoreyButler.NVMforWindows
インストールが完了しても慌てない。
まずは Windows PowerShell を静かに閉じ、心を落ち着かせて再度 Windows PowerShell を起動する。
> nvm version
1.1.12
無事インストールができたようだ。
Node.js
ようやく Node.js をインストールできる。
nvm を使用して、インストールが可能なバージョンを確認する。
> nvm list available
| CURRENT | LTS | OLD STABLE | OLD UNSTABLE |
|--------------|--------------|--------------|--------------|
| 22.3.0 | 20.14.0 | 0.12.18 | 0.11.16 |
| 22.2.0 | 20.13.1 | 0.12.17 | 0.11.15 |
| 22.1.0 | 20.13.0 | 0.12.16 | 0.11.14 |
LTS の最新版である 20.14.0 を nvm でキメる。
> nvm install 20.14.0
Downloading node.js version 20.14.0 (64-bit)...
Extracting node and npm...
Complete
npm v10.7.0 installed successfully.
Installation complete. If you want to use this version, type
nvm use 20.14.0
> nvm use 20.14.0
Now using node v20.14.0 (64-bit)
> node -v
v20.14.0
npm
Node.js をインストールしたら Node.js の パッケージを管理するツールもついてきた。
助かる。
> npm -v
10.7.0
Next.js
npm に同梱されている npx コマンドを使用して Next.js をインストールする。
> npx create-next-app
Need to install the following packages:
create-next-app@14.2.4
Ok to proceed? (y) y
? What is your project named? » next-app
? Would you like to use TypeScript? » No / Yes
Yes
? Would you like to use ESLint? » No / Yes
Yes
? Would you like to use Tailwind CSS? » No / Yes
Yes
? Would you like to use `src/` directory? » No / Yes
No
? Would you like to use App Router? (recommended) » No / Yes
Yes
? Would you like to customize the default import alias (@/*)? » No / Yes
No
Creating a new Next.js app in C:\dev\next-app.
Using npm.
Initializing project with template: app-tw
Installing dependencies:
- react
- react-dom
- next
Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
- eslint
- eslint-config-next
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated @humanwhocodes/config-array@0.11.14: Use @eslint/config-array instead
npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
added 361 packages, and audited 362 packages in 3m
137 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Success! Created next-app at C:\dev\next-app
TypeScript - React - Next.js とその他諸々をインストールできた。
> cd .\next-app\
next-app> npm run dev
> next-app@0.1.0 dev
> next dev
▲ Next.js 14.2.4
- Local: http://localhost:3000
✓ Starting...
✓ Ready in 1694ms
サーバーを起動して http://localhost:3000 の表示確認をする。
axios
API となんか良い感じに通信するには
JavaScript で使用できる Promise ベースの HTTP クライアントライブラリがなんか良いらしい。
インストールする。
next-app> npm install axios
added 9 packages, and audited 371 packages in 4s
138 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Laravel API 連携
https://zenn.dev/mkt_engr/articles/axios-req-res-typescript を参考にさせていただくことで
とりあえず API は叩けた。
新規登録
http://localhost:3000/api/users/create
'use client';
import React, {useState, useEffect} from 'react'
import axios, {AxiosRequestConfig, AxiosResponse, AxiosError} from 'axios';
type USER = {
id: number, name: string, email: string
};
const url = 'http://localhost:8000/api';
const data = {
'name': 'Test5',
'email': 'test5@example.com',
'password': 'password',
};
const options: AxiosRequestConfig = {
url: `${url}/users`,
method: 'POST',
data: data,
};
const AxiosPost: React.FC = () => {
const [user, setUser] = useState<USER>({} as USER);
// API 通信を行う箇所
useEffect(() => {
axios(options)
.then((response: AxiosResponse<USER>) => {
setUser(response.data.user);
})
.catch((e: AxiosError<{ error: string }>) => {
// エラー処理
console.log(e.message);
});
}, []);
// 登録したユーザー情報を表示する箇所
return (
<div>
<ul>
<li key={user.id}>
{user.id} : {user.name} : {user.email}
</li>
</ul>
</div>
);
};
export default AxiosPost;
一覧
http://localhost:3000/api/users
'use client';
import React, {useState, useEffect} from 'react'
import axios, {AxiosRequestConfig, AxiosResponse, AxiosError} from 'axios';
type USER = {
id: number, name: string, email: string
};
const url = 'http://localhost:8000/api';
const options: AxiosRequestConfig = {
url: `${url}/users`,
method: 'GET',
};
const AxiosGet: React.FC = () => {
const [users, setUsers] = useState<USER[]>([]);
// API 通信を行う箇所
useEffect(() => {
axios(options)
.then((response: AxiosResponse<USER[]>) => {
setUsers(response.data.users);
})
.catch((e: AxiosError<{ error: string }>) => {
// エラー処理
console.log(e.message);
});
}, []);
// ユーザー情報を表示する箇所
return (
<div>
<ul>
{users.map(({id, name, email}) => {
return (
<li key={id}>
{id} : {name} : {email}
</li>
);
})}
</ul>
</div>
);
};
export default AxiosGet;
おわり
自身の JavaScript ヂカラが赤ちゃんすぎることを再認識。大人しく jQuery ワールドに帰ります。
Discussion