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?

Route HandlerによるMock APIの作成方法

Last updated at Posted at 2026-01-04

0. はじめに

学んだ技術や知識についてを備忘録としてまとめることを目的にしている。

0.1. 開発環境

  • OS: macOS Sequoia 15.5
  • エディタ: VS Code
  • ライブラリ: React 19.2.0
  • フレームワーク: Next.js 16.0.5
    • ルーター: App Router
プロジェクト作成方法

ターミナルにてプロジェクトを作成したいディレクトリ内に移動して、以下のコマンドを実行。

ターミナル
# プロジェクトの雛形を作成
npx create-next-app

プロジェクト作成時の質問は以下の通り。

スクリーンショット 2025-11-29 18.39.53.png

作成が完了したら以下のコマンドで起動

npm run dev

1. 基本知識

1.1. APIとは

Application Programming Interfaceの略。システムの実装を隠蔽し、機能と利用手順のみを外部に公開するインターフェースである。

1.1.1. Web API

HTTP/HTTPSプロトコルを用いて、ネットワークを経由してプログラム間でデータのやり取りや機能の呼び出しを行うインターフェース。プログラムがローカル環境ではなくURI(Uniform Resource Identifier)で特定されるエンドポイントに対してリクエストを送り、メソッド(GET, POST, PUT, DELETEなど)によって操作を定義する。本記事で使うAPIとは主に「Web API」を指すこととする。

1.1.2. OS API

オペレーティングシステムAPI。アプリケーションがハードウェア資源(CPU、メモリ、ディスク、ネットワークなど)を安全に利用するためにOSが提供するインターフェース。ハードウェアの直接操作を抽象化・保護し、プログラムがOSの種類に依存せずリソースを使えるようにする。

例:

  • Windows API: Windows OSの昨日(ウィンドウ操作、ファイル管理、システム情報取得など)をC言語やC++などのプログラムから利用できるように提供されたAPI

1.1.3. ライブラリ API

プログラムの特定の機能を実行するために、外部から呼び出せる関数、クラス、メソッドの集合(インターフェース)。

1.1.4. フレームワーク API

特定のソフトウェアフレームワークが持つ「制御の反転」構造を維持しつつ、開発者がそのフレームワークの機能にアクセスし、自身のコードをシステムに組み込むために提供される公開インターフェースの集合。

例:

  • Android Framework API: OSというフレームワーク上でアプリを動かすためのAPI

1.1.5. ハードウェア API

ソフトウェアがハードウェア(GPUやオーディオチップなど)の機能を直接、またはドライバーを介して制御するためのインターフェース。

例:

  • CUDAやOpenCL

1.1.6. ABI

application。ソースコードレベルではなくバイナリレベルでのインターフェース。

1.1.7. 通信プロトコル API※Web API以外のリモートAPI

ネットワークを介するものの、Web標準(HTTP/JSON)とは異なるプロトコルや方式を用いたもの。

例:

  • RPC: Remote Procedure Call。ネットワーク上の別サーバーにある関数をあたかもローカルの関数であるかのように呼び出すAPI

1.2. Mock APIとは

開発時において、本来想定しているWeb APIが完成されていない、あるいは利用できない状況下で使用する仮のWeb APIのこと。

1.3. Route Handlerとは

Next.jsにおいて、アプリケーション内に独自のAPIエンドポイントを作成する仕組み。Web標準のFetch APIに準拠している。Next.jsのRoute Handlerは、appディレクトリ内の構造に従い、APIエンドポイントとして機能させるには、route.tsというファイル名を使用する。例えば、エンドポイントが/userであった場合、/app/user/route.tsとなるようにroute.tsファイルを作成すればよい。ただし、本記事では便宜上Mock APIは/app/apiディレクトリ内にまとめることとする。

2. Mock API作成

2.1. GETメソッド

API仕様が以下であるとし、これに対するMock APIを作成する。

  • エンドポイント: GET /search

  • パスパラメータ

    パラメータ 必須 説明
    q string 任意 ユーザー名の部分一致検索キーワード

レスポンス例:

GET /search
[
  { "id": 1, "name": "A" },
  { "id": 2, "name": "B" }
]

2.1.1. Route Handlerの作成

以下のディレクトリ構成でroute.tsを作成する。

src
└─ app
   └─ api
      └─ search
         └─ route.ts  ← 作成
/search/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  // URLから探索クエリを取得
  const searchParam = request.nextUrl.searchParams;
  const query = searchParam.get('q');

  // モックデータ
  const mockData = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Ben" },
    { id: 3, name: "Cecil" },
    { id: 4, name: "Dominic" }
  ];

  const filteredData = query
    ? mockData.filter(obj => obj.name.includes(query))
    : mockData;

  // レスポンスを返す
  return NextResponse.json(filteredData);
}

2.1.2. 型の定義

以下のディレクトリ構成で型定義ファイルを作成する。

src
└─ app
   ├─ api
   └─ types
      └─ user.ts  ← 作成
レスポンスデータの型を定義
export interface UserResponse {
  id: number;
  name: string;
}

2.1.3. fetch関数の作成

'use client';

import { useState } from 'react';
import { UserResponse } from "./types/user";

export default function Home() {

  // APIで取得したデータを管理
  const [results, setResults] = useState([]);
  const [error, setError] = useState<string | null>(null);

  const baseUrl = 'http://localhost:3000'
  const token = "dummy_token"

  // GET /search
  const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => {

    const query = e.target.value;
    // 入力が空の場合はリセットして終了
    if (!query) {
      setResults([]);
      setError(null);
      return;
    }
    const url = `${baseUrl}/api/search?q=${query}`;

    try {
      // APIデータを取得
      const response = await fetch(url, {
        method: "GET",
        headers: {
          // 認証用トークン
          'Authorization': `Bearer ${token}`,
          // JSON形式でデータを受け取りたいことを明示
          'Accept': 'application/json',
        }
      });

      if (!response.ok) {
        throw new Error(`エラーが発生しました(Status: ${response.status})`);
      }

      // JSON形式のデータを取得
      const data = await response.json();
      setResults(data);

    } catch (err) {
      // ネットワークエラーや上記のエラーをキャッチ
      setError(err instanceof Error ? err.message : "予期せぬエラーが発生しました");
      setResults([]); // リストをクリア
    }
  }

  return (
    <div>
      <main>
        <input
          type="text"
          onChange={handleSearch}
          placeholder="名前で検索..."
          className="border p-2"
        />
        {error && <p className="text-red-500">{error}</p>}
        {!error && results.length === 0 && (
          <p className="text-gray-400">該当するユーザーはいません</p>
        )}
        <ul>
          {results.map((user: UserResponse) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      </main>
    </div>
  );
}
Acceptヘッダー GETリクエストでは、リクエストを送る側がサーバーからのレスポンスをどのような形式(JSON, XMLなど)で受け取りたいかをサーバーに伝えるために、Acceptヘッダーを使用する。

2.1.4. 実行結果

画面収録 2026-01-04 17.04.54.gif

2.2. POSTメソッド

API仕様が以下であるとし、これに対するMock APIを作成する。

  • エンドポイント: POST /users

  • リクエストボディ

    パラメータ 必須 説明
    name string 必須 ユーザー氏名
    email string 必須 ユーザーのメールアドレス

レスポンス例:

201 Created
{
  "id": "452",
  "name": "山田 太郎",
  "email": "yamada@example.com",
  "createdAt": "2024-05-20T10:00:00.000Z"
}
400 Bad Request
{ "error": "Name and Email are required" }

2.2.1. Route Handlerの作成

import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  // リクエストボディの解析
  const body = await request.json();
  const { userName, email } = body;

  if (!userName || !email) {
    return NextResponse.json(
      { error: "Name and Email are required" },
      { status: 400 }
    );
  }

  const newMockData = {
    id: Math.floor(Math.random() * 1000).toString(),
    userName,
    email,
    createdAt: new Date().toISOString(),
  };

  return NextResponse.json(newMockData, { status: 201 });
}

2.2.2. 型の定義

export interface UserRequest {
  userName: string;
  email: string;
}

2.2.3. fetch関数の作成

'use client';

import Dropdown from "@/components/Dropdown";
import { useState } from 'react';
import { UserRequest } from "./types/user";

export default function Home() {

  // APIで取得したデータを管理
  const [error, setError] = useState<string | null>(null);
  const [userName, setUserName] = useState("")
  const [email, setEmail] = useState("")

  const baseUrl = 'http://localhost:3000'
  
  // POST /users
  const handleSubmit = async (e: React.FormEvent) => {
    // デフォルトのイベント動作をキャンセル
    e.preventDefault();
    const url = `${baseUrl}/api/users`;
    const requestBody: UserRequest = { userName, email };

    try {
      const response = await fetch(url,
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(requestBody),
        });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || "登録に失敗しました");
      }

      // リクエスト後にリセット
      setUserName("");
      setEmail("");
    } catch (err) {
      // ネットワークエラーや上記のエラーをキャッチ
      setError(err instanceof Error ? err.message : "予期せぬエラーが発生しました");
    }
  }

  return (
    <div>
      <main>
        <div>
          <form onSubmit={handleSubmit} className="flex flex-col gap-4">
            <input
              type="text"
              placeholder="名前"
              value={userName}
              onChange={(e) => setUserName(e.target.value)}
              className="border p-2"
            />
            <input
              type="email"
              placeholder="メールアドレス"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className="border p-2"
            />
            <button type="submit" className="bg-blue-500 text-white p-2 rounded">
              ユーザー登録
            </button>
          </form>
        </div>
      </main>
    </div>
  );
}
Content-Typeヘッダー

Content-Typeヘッダーは、リクエストボディ(body)が含まれている場合に、そのボディ内のデータがどのような形式(メディアタイプ)であるかをサーバーに伝えるために使用する。
よって、主にPOST/PUT/PATCHに使用する。
GETメソッドにおいては、サーバーからデータを取得することを目的としており、通常リクエストボディを持たない。よって、ボディの内容形式を示すContent-Typeは不要となる。データ取得の条件(フィルタリングなど)は、URLのクエリパラメータとして送信される。

2.2.4. 実行結果

POSTリクエストが正しく送られているか確認するには、ブラウザのデベロッパーツールのネットワークタブを開き、送信されたリクエストのペイロードレスポンスを確認する。

スクリーンショット 2026-01-04 18.25.34.png

スクリーンショット 2026-01-04 18.25.50.png

2.3. PUTメソッド

API仕様が以下であるとし、これに対するMock APIを作成する。

  • エンドポイント: PUT /users/[id]

  • パスパラメータ

    パラメータ 説明
    id string 更新したいユーザーのID
  • リクエストボディ

    パラメータ 必須 説明
    name string 任意 新しい氏名
    email string 任意 新しいメールアドレス

レスポンス例:

200 OK
{
  "id": "452",
  "name": "山田 太郎",
  "email": "yamada@example.com",
  "createdAt": "2024-05-20T10:00:00.000Z"
}

2.3.1. Route Handlerの作成

import { UserRequest } from "@/app/types/user";
import { NextResponse } from "next/server";

export async function PUT(
  request: Request,
  { params }: { params: { id: string } }
) {
  // 更新対象のID
  const id = params.id;
  const body = await request.json();
  const { userName, email }: UserRequest = body;

  // 更新後のデータを想定してレスポンスを作成
  const MockData = {
    id,
    name: userName || "未設定",
    email: email || "未設定",
    updatedAt: new Date().toISOString(),
  };

  return NextResponse.json(MockData, { status: 200 });
}
![スクリーンショット 2026-01-04 19.36.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4010745/0f40d2ce-9e72-4530-ba17-99f0c53ee1d1.png)

2.3.2. 型の定義

2.2.2.型の定義と同じ。

2.3.3. fetch関数の作成

'use client';

import { useState } from 'react';
import { UserRequest } from './types/user';

export default function HOme() {
  const [targetId, setTargetId] = useState(""); // 更新対象のID
  const [userName, setUserName] = useState("");
  const [email, setEmail] = useState("");
  const [error, setError] = useState<string | null>(null);

  const baseUrl = 'http://localhost:3000'

  // PUT /users/[id]
  const handleUpdate = async (e: React.FormEvent) => {
    e.preventDefault();

    // IDが未入力の場合は中断
    if (!targetId) {
      setError("更新対象のIDを入力してください");
      return;
    }

    const url = `${baseUrl}/api/users/${targetId}` // パスにIDを埋め込む
    const requestBody: UserRequest = { userName, email };

    try {
      const response = await fetch(url,
        {
          method: "PUT",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify(requestBody),
        });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || "更新に失敗しました");
      }

      // リクエスト後にリセット
      setTargetId("");
      setUserName("");
      setEmail("");
    } catch (err) {
      setError(err instanceof Error ? err.message : "予期せぬエラーが発生しました");
    }
  };

  return (
    <div>
      <form onSubmit={handleUpdate} className="flex flex-col">
        {/* ID入力フィールド */}
        <div>
          <label>更新対象のID</label>
          <input
            type="text"
            placeholder="例: 1"
            value={targetId}
            onChange={(e) => setTargetId(e.target.value)}
            className="border rounded"
          />
        </div>
        {/* データ入力フィールド */}
        <div>
          <label>新しい名前</label>
          <input
            type="text"
            placeholder="名前"
            value={userName}
            onChange={(e) => setUserName(e.target.value)}
            className="border rounded"
          />
        </div>
        <div>
          <label>新しいメールアドレス</label>
          <input
            type="email"
            placeholder="メールアドレス"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className="border rounded"
          />
        </div>

        <button type="submit" className="rounded">
          情報を更新する
        </button>
      </form>

      {/* フィードバック表示 */}
      {error && <p className="text-red-500">{error}</p>}
    </div>
  );
}

2.3.4. 実行結果

スクリーンショット 2026-01-04 19.29.07.png

スクリーンショット 2026-01-04 19.36.25.png

スクリーンショット 2026-01-04 19.38.04.png

参考資料

記事作成にあたり、以下のサイトを参考にさせて頂きました。心より感謝申し上げます。

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?