はじめに
こんにちは。ソーイ株式会社の工藤です。
業務では、LaravelでAPIを実装したあと、フロントから呼び出して画面にデータを出す、という流れが日常的にあります。Postmanで動作確認できた後、フロントとの繋ぎ込みを行うことで実際のサービスとして機能させます。
前回の記事では、Postmanを使って「APIを叩く」基本を学びました。今回はその続きとして、LaravelでAPIを作り、JavaScriptから呼び出すまでを実際のコードで整理します。
検証環境・利用パッケージ
本記事のサンプルコードは、次のバージョンを前提にしています。
| ライブラリ | バージョン | 用途 |
|---|---|---|
| Laravel | 11.x(検証時: 11.16) | API本体のフレームワーク |
| Laravel Passport | 12.x(検証時: 12.2) | API認証(OAuth 2.0) |
認証には Laravel Passport を使います。現在のLaravelでは、SPAやモバイル向けのAPI認証に Laravel Sanctum を使う構成が一般的です。一方で、第三者アプリ連携や認可コードフローなど OAuth 2.0 の仕組みが必要な場合 は Passport が向いています。本記事では OAuth 認証を提供するために Passport を採用しています。
Passport のインストール手順や Client 発行までは本記事の範囲外とします。すでにアクセストークンを発行できる状態を前提に進めます。
結論
LaravelでAPIを作るときは「ルート定義 → Controller → JSONレスポンス」の流れで実装します。フロント側はそのエンドポイントに対して fetch でリクエストを送り、レスポンスを画面に反映します。Postmanで叩いていたことを、コードが代わりに行うイメージです。
想定読者
- 前回記事でPostmanの基本を学んだ方
- LaravelでAPIを作ったことはあるが、フロントへの組み込み方がイメージできていない方
- APIが実際にどう使われるか知りたい初心者エンジニア
この記事でわかること
- LaravelでAPIを実装する流れ(ルート定義 → Controller → JSONレスポンス)
- 基本的な CRUD(GET / POST / PUT / DELETE)のサンプル
- Controller・FormRequest・Resourceの役割と使い分け
-
fetchでAPIを呼び出し、レスポンスを画面に反映するまでの実装 - フロント連携時にハマりやすい4つのポイントとその対処
今回の構成概要
今回のサンプルは、ユーザーを扱う REST API です。フロントのJavaScriptがLaravel APIにリクエストを送り、DBのデータをJSON形式で受け取って画面に反映します。一覧取得の流れは次の4ステップです。
- フロントが Bearer Token 付きでリクエストを送る
- Laravel API がデータベースからユーザーを取得する
- 取得結果が API に返る
- API が JSON をフロントに返し、画面にユーザー一覧を表示する
エンドポイントは次のとおりです(routes/api.php はデフォルトで /api が付きます)。トークン発行だけは Passport 標準の /oauth/token を使います。
| メソッド | パス | 処理 | 認証 |
|---|---|---|---|
POST |
/oauth/token |
アクセストークン発行(Passport) | 不要 |
GET |
/api/v1/health |
ヘルスチェック(疎通確認) | 不要 |
GET |
/api/v1/users |
ユーザー一覧 | 必要 |
POST |
/api/v1/users |
ユーザー登録 | 必要 |
PUT |
/api/v1/users/{id} |
ユーザー更新 | 必要 |
DELETE |
/api/v1/users/{id} |
ユーザー削除 | 必要 |
① LaravelでAPIを作る
ルーティング(routes/api.php)
どのURLにアクセスした時に、どのControllerを呼び出すかを定義するファイルです。クライアント種別でグループを分けておくと管理しやすくなります。
コメントには「定義したパス」と「実際に叩くURL」を書いておくと、フロント実装やPostman確認のときに迷いにくくなります。
// routes/api.php
// ※ RouteServiceProvider / bootstrap の設定により、このファイルのルートには /api が付く
// ※ トークン発行は Passport 標準: POST /oauth/token(このファイルには書かない)
Route::prefix('v1')->group(function () {
// 認証不要のルート(疎通確認の例)
// 定義: GET /health → 実際のパス: GET /api/v1/health
Route::get('/health', [HealthController::class, 'show']);
// アクセストークンが必要なルート(Laravel Passport 12.x / auth:api)
// Authorization: Bearer {POST /oauth/token で取得したアクセストークン}
Route::middleware('auth:api')->group(function () {
// 定義: GET /users → 実際のパス: GET /api/v1/users
Route::get('/users', [UserController::class, 'index']);
// 定義: POST /users → 実際のパス: POST /api/v1/users
Route::post('/users', [UserController::class, 'store']);
// 定義: PUT /users/{id} → 実際のパス: PUT /api/v1/users/{id}
Route::put('/users/{id}', [UserController::class, 'update']);
// 定義: DELETE /users/{id} → 実際のパス: DELETE /api/v1/users/{id}
Route::delete('/users/{id}', [UserController::class, 'destroy']);
});
});
実装ファイルの構成と役割
LaravelでAPIを実装する際、主に3種類のファイルが登場します。リクエストが来てからJSONが返るまでの流れは、次の図のとおりです。
| ファイル | 役割 |
|---|---|
| Controller | リクエストを受け取り、処理してレスポンスを返す |
| FormRequest | リクエストの入力値を検証(バリデーション)する |
| Resource | データをAPIレスポンス用のJSON形式に整形する |
Controller単体にすべてを書くと肥大化しやすいため、それぞれ役割を分けておくのが実務での定石です。以下、この3つを順番に見ていきます。
Controller
リクエストの受け取りとレスポンスの返却を担います。一覧・登録・更新・削除の4つを揃えておくと、APIとしての基本形がイメージしやすくなります。
// app/Http/Controllers/Api/V1/UserController.php
class UserController extends Controller
{
// 一覧取得:検証すべき入力値がないため、FormRequest は受け取らない
public function index()
{
$users = User::select('id', 'name', 'email')->get();
return UserResource::collection($users);
}
// POST /api/v1/users
// 登録:入力値の検証が必要なので、FormRequest を受け取る
public function store(StoreUserRequest $request)
{
$user = User::create($request->validated());
return (new UserResource($user))
->response()
->setStatusCode(201);
}
// PUT /api/v1/users/{id}
// 更新:存在確認 + 入力値の検証
public function update(UpdateUserRequest $request, int $id)
{
$user = User::findOrFail($id);
$user->update($request->validated());
return new UserResource($user);
}
// DELETE /api/v1/users/{id}
// 削除:成功時は 204 No Content を返すことが多い
public function destroy(int $id)
{
$user = User::findOrFail($id);
$user->delete();
return response()->noContent(); // 204
}
}
データ件数が増える一覧APIでは、
get()で全件返すとレスポンスが肥大化します。実務ではpaginate()でページ分割するのが基本です。今回はJSONの形をシンプルに保つためget()を使っています。
FormRequest(リクエストの検証)
リクエストの入力値を検証するファイルです。rules() に定義したルールを満たさないリクエストは、Controllerに到達する前に自動で422エラーとして弾かれます。Controller側に if を並べる必要がなくなるのが利点です。
登録用の StoreUserRequest と、更新用の UpdateUserRequest を分けて定義します。更新時は unique ルールで「自分自身のメールアドレス」を除外するのがポイントです。
// app/Http/Requests/Api/V1/User/StoreUserRequest.php
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
];
}
}
// app/Http/Requests/Api/V1/User/UpdateUserRequest.php
class UpdateUserRequest extends FormRequest
{
public function rules(): array
{
$userId = $this->route('id');
return [
'name' => ['required', 'string', 'max:255'],
// 自分自身の email は unique チェックから除外する
'email' => ['required', 'email', "unique:users,email,{$userId}"],
];
}
}
検証に失敗すると、Laravelは次のようなJSONを422で返します。このレスポンス形式は、後述する「エラーハンドリング」でフロント側が利用します。
{
"message": "The email field is required.",
"errors": {
"email": ["The email field is required."]
}
}
Resource(レスポンスの整形)
DBから取得したデータをAPIレスポンス用のJSON形式に変換するファイルです。「不要なフィールドを除外する」「フロントが使いやすい形に整形する」といった処理をここに集約できます。パスワードのハッシュや内部管理用のカラムをうっかり返してしまう事故を防げるのも大きな利点です。
// app/Http/Resources/Api/V1/UserResource.php
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}
フロントがこのAPIを呼び出したとき、以下のJSONが返ってきます。data というキーで包まれているのは、Resourceを返すとLaravelが自動的にラップしてくれるためです。
{
"data": [
{ "id": 1, "name": "Taro Yamada", "email": "taro@example.com" },
{ "id": 2, "name": "Hanako Sato", "email": "hanako@example.com" }
]
}
Postmanで動作確認する
コードが書けたら、フロントに進む前にPostmanで確認します。まずは GET /api/v1/users に対して、ヘッダーに以下の2つを付けてリクエストを送ってください。
| ヘッダー | 値 |
|---|---|
Authorization |
Bearer {POST /oauth/token で取得したアクセストークン} |
Accept |
application/json |
ユーザー一覧のJSONが返ってくればOKです。あわせて POST / PUT / DELETE も同じヘッダーで確認しておくと安心です。POST と PUT ではボディにJSONを送り、Content-Type: application/json も付けます。返ってこない場合はフロントを疑う前にAPI側を直しましょう。切り分けの順番を守ると、原因追及がぐっと楽になります。
② フロントからAPIを呼び出す
LaravelのAPIができたら、JavaScriptの fetch を使って呼び出します。Postmanでやっていた「ヘッダーを付けてリクエストを送る」操作を、そのままコードに置き換えるイメージです。
共通部分を先に用意します。
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
function authHeaders() {
const token = localStorage.getItem('access_token');
return {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
};
}
一覧取得(GET)は次のとおりです。
// GET /api/v1/users
async function getUsers() {
const response = await fetch(`${API_BASE_URL}/api/v1/users`, {
method: 'GET',
headers: authHeaders(),
});
if (!response.ok) {
throw new Error(`APIエラー: ${response.status}`);
}
const json = await response.json();
return json.data; // Laravelが返した { data: [...] } の中身
}
登録・更新・削除も同じパターンです。ボディを送るときは Content-Type: application/json を追加します。
// POST /api/v1/users
async function createUser(payload) {
const response = await fetch(`${API_BASE_URL}/api/v1/users`, {
method: 'POST',
headers: {
...authHeaders(),
'Content-Type': 'application/json',
},
body: JSON.stringify(payload), // { name, email }
});
if (!response.ok) {
throw new Error(`APIエラー: ${response.status}`);
}
const json = await response.json();
return json.data;
}
// PUT /api/v1/users/{id}
async function updateUser(id, payload) {
const response = await fetch(`${API_BASE_URL}/api/v1/users/${id}`, {
method: 'PUT',
headers: {
...authHeaders(),
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`APIエラー: ${response.status}`);
}
const json = await response.json();
return json.data;
}
// DELETE /api/v1/users/{id}
async function deleteUser(id) {
const response = await fetch(`${API_BASE_URL}/api/v1/users/${id}`, {
method: 'DELETE',
headers: authHeaders(),
});
if (!response.ok) {
throw new Error(`APIエラー: ${response.status}`);
}
// 204 No Content の場合、ボディはない
}
ポイントは3つです。
- APIのURLは環境変数から読み込む:ローカル・ステージング・本番でURLが変わるため、コードに直接書かない
-
Accept: application/jsonを付ける:理由は後述します -
Content-Typeはボディを送るときだけ:GET / DELETE では不要、POST / PUT では必要
取得した json.data を、使用しているフレームワーク(NuxtやReactなど)のリアクティブな仕組みにセットすれば、画面に自動的に反映されます。
アクセストークンの保存先として
localStorageは手軽ですが、XSSの脆弱性があるとJavaScriptから盗み出されます。認証情報を扱う本番のアプリケーションでは、HttpOnly属性を付けたCookieでの管理も選択肢になります。チームの方針を確認しておきましょう。
③ フロント連携時にハマりやすいポイント
Postmanでは動いていたAPIが、フロントのコードから呼ぶと動かない。よくあるパターンを4つ挙げます。
1. Accept: application/json の付け忘れ
Laravelは「リクエスト元がJSONを期待しているか」をこのヘッダーで判断します。付いていないと、バリデーションエラーや認証エラーがJSONではなくリダイレクトやHTMLで返ってくることがあります。
フロント側で「なぜかJSONのパースに失敗する」「エラーの内容が取れない」と感じたら、まずこのヘッダーを確認してください。
2. CORSエラー
「Postmanでは動いたのに、フロントから呼ぶと動かない」とき、原因の多くがCORSです。ブラウザには「異なるドメイン・ポートへのアクセスを制限する」というセキュリティの仕組みがあり、Postmanはこの制限を受けませんが、fetch は受けます。
解決にはAPI側(Laravel)で、アクセスを許可するオリジンを設定します。フロントだけでは直せない点に注意してください。
// config/cors.php
return [
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
'allowed_headers' => ['*'],
'supports_credentials' => false,
];
allowed_origins に *(すべて許可)を指定しつつ supports_credentials を true にすることはできません。Cookieを使う構成では、オリジンを明示的に列挙する必要があります。
なおこの設定はバックエンド側の対応になるため、フロント担当者だけでは解決できません。担当が分かれている場合は、許可してほしいオリジンを伝えて依頼しましょう。
3. トークン切れ(401エラー)
アクセストークンの有効期限が切れると、LaravelはHTTP 401を返します。フロント側で401を検知したら、ログイン画面にリダイレクトするのが最もシンプルな対応です。
リフレッシュトークンを発行している構成(Passportのpassword grantなど)であれば、401を受けたタイミングで新しいアクセストークンを取得し、元のリクエストを再送する実装も可能です。その場合、再送も401になったときに無限ループしないよう、リトライは1回までに制限してください。
4. エラーハンドリング
Postmanでは200 OKが返れば確認終了ですが、実装ではエラー時の処理も必要です。Laravelはバリデーションエラーを422、未認証を401、サーバーエラーを500で返すので、ステータスコードごとに処理を分けます。
if (!response.ok) {
const body = await response.json().catch(() => ({}));
if (response.status === 401) {
redirectToLogin();
}
// 422 の場合、body.errors に項目ごとのメッセージが入っている
// 例: { email: ['メールアドレスの形式が正しくありません'] }
if (response.status === 422) {
showFieldErrors(body.errors ?? {});
}
throw new Error(body.message ?? `APIエラー: ${response.status}`);
}
422のときは errors を入力欄の下に表示し、500のときは「時間をおいて再度お試しください」のような汎用メッセージを出す。このように出し分けておくと、ユーザーが次に何をすればいいのか分かるようになります。
まとめ
- LaravelのAPI実装は「ルート定義 → Controller → JSONレスポンス」の流れ
- 今回のサンプルでは GET / POST / PUT / DELETE の基本CRUDを揃えている
- Controller・FormRequest・Resourceがそれぞれ「受け取り・検証・整形」を担う
- 認証は OAuth が必要な場合に Passport を使う(SPA向けなら Sanctum が一般的)
- フロント側は、Postmanで送っていたリクエストをそのまま
fetchのコードに置き換えるイメージ - フロント連携時は「
Acceptヘッダー・CORS設定・401対応・エラーハンドリング」の4点を押さえておく
参考資料
- API基礎(初学者向け)- Zenn
- APIの仕組みが分かる・使いこなせる人材になれる記事 - Qiita
- CORSをざっくり理解し、CORSエラーを解決する - Zenn
- Webアプリ開発のフロントエンドとバックエンドの結合方法 - Qiita
お知らせ
技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!
https://qiita.com/organizations/sewii


