2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Next.js】API Route のリクエスト処理・型安全・例外処理まとめ

2
Posted at

はじめに

Next.js の App Router で EC サイトの注文管理 API を実装する中で、リクエストの受け取り方・型安全・例外処理・アーキテクチャ設計について多くの学びがありました。この記事では、API Route 実装時につまずきやすいポイントを整理してまとめます。

この記事の対象読者・前提条件

対象読者

  • Next.js App Router で API Route を書き始めた方
  • TypeScript の型安全をどこまでやるべきか迷っている方

前提条件

  • Next.js の基本的なプロジェクト構成を理解している
  • Prisma の基本操作(CRUD)を知っている

実装

1. request.json() でのデータ受け取り方

request.json() は非同期関数なので、必ず await が必要です。

// ❌ await がないと Promise オブジェクトが返る
const { status } = request.json();

// ✅ await でデータの到着を待つ
const { status } = await request.json();

解説:

リクエストボディのデータはネットワーク経由で送られてくるため、すべてのデータが届くまで待つ必要があります。await をつけないと、データの中身ではなく「データを取得中」という Promise オブジェクトが返ってきてしまいます。Java で例えると、InputStream からデータを読み取る操作に近い考え方です。

また、リクエストボディの受け取りはシンプルに分割代入で書けます。

// ❌ 不要なラッパーオブジェクトで受け取る
const { paymentRequest } = await request.json();
const amount = paymentRequest.amount;

// ✅ 直接必要な値を取り出す
const { amount } = await request.json();

2. request.json() の型安全

request.json() の戻り値は any 型になります。TypeScript はリクエストの中身を知りようがないためです。

// ❌ any 型のまま使うと型チェックが効かない
const { userId, address, items } = await request.json();
// items.map((item) => ...) の item も any 型になる

// ✅ 型を定義して指定する
type OrderRequest = {
  userId: string;
  address: string;
  items: {
    productId: string;
    quantity: number;
    price: number;
  }[];
};

const { userId, address, items } = await request.json() as OrderRequest;

解説:

型定義が必要かどうかの判断基準は「TypeScript が自分で型を判断できるかどうか」です。特に外部からやってくるデータ(API レスポンス、リクエストボディ)は型を教えてあげる必要があります。逆に useState("") のように初期値から推論できる場合は不要です。

3. オプショナルチェーンの不要な使用

// ❌ ガード処理の後なのに ?. を使っている
if (!session?.user?.email) {
  return NextResponse.json({ message: "未認証" }, { status: 401 });
}
const email = session.user?.email; // ← 不要な ?.

// ✅ ガード後は安全にアクセスできる
if (!session?.user?.email) {
  return NextResponse.json({ message: "未認証" }, { status: 401 });
}
const email = session.user.email; // ← ?. 不要

解説:

ガード処理(early return)で email の存在を保証した後は、オプショナルチェーン ?. は不要です。むしろ不要な ?. があると「この値は null かもしれない」という誤ったメッセージをコードの読者に伝えてしまいます。

4. NextResponse.json() の引数の違い

// ❌ ステータスコードがボディに入ってしまう
return NextResponse.json({ status: 200 });

// ✅ 第1引数がボディ、第2引数が設定
return NextResponse.json({ orders: result }, { status: 200 });

// ✅ 200 はデフォルトなので省略可能
return NextResponse.json({ orders: result });

解説:

  • 第1引数:レスポンスのボディ(クライアントに返すデータ)
  • 第2引数:レスポンスの設定(ステータスコードなど)

レスポンスのキー名も data のような汎用的な名前より、orders のように中身が明確な名前にすると可読性が上がります。

5. 例外処理

// ❌ try/catch がない(DB 接続エラーや存在しない ID に対応できない)
export async function PATCH(
  request: Request,
  { params }: { params: { id: string } }
) {
  const { status } = await request.json();
  const result = await prisma.order.update({
    where: { id: params.id },
    data: { status },
  });
  return NextResponse.json({ data: result });
}

// ✅ エラーハンドリングを追加
export async function PATCH(
  request: Request,
  { params }: { params: { id: string } }
) {
  try {
    const { status } = await request.json();
    const result = await prisma.order.update({
      where: { id: params.id },
      data: { status },
    });
    return NextResponse.json({ data: result });
  } catch (error) {
    return NextResponse.json(
      { message: "更新に失敗しました" },
      { status: 500 }
    );
  }
}

フロント側でも response.ok のチェックが必要です。

const response = await fetch("/api/orders");
if (!response.ok) {
  // エラー処理
}
const data = await response.json();

6. useEffect 依存配列の注意点

// ❌ params.id を使っているのに依存配列が空
useEffect(() => {
  fetch(`/api/orders/${params.id}`);
}, []);

// ✅ 使っている外部の値を依存配列に入れる
useEffect(() => {
  fetch(`/api/orders/${params.id}`);
}, [params.id]);

解説:

依存配列には「この値が変わったら再実行してほしい」ものをすべて入れます。params.id が変わっても再フェッチされないと、古いデータが表示されたままになります。

アーキテクチャ:レイヤー構造と責務分離

EC サイトの注文管理機能のアーキテクチャを整理すると、以下のようになります。

[クライアント層]
orders/page.tsx        — 注文一覧(表示 + fetch)
orders/[id]/page.tsx   — 注文詳細(表示 + fetch)
components/OrderStatusBadge.tsx — ステータス表示

[API層]
api/orders/route.ts       — GET(一覧)/ POST(注文作成)
api/orders/[id]/route.ts  — GET(詳細)/ PATCH(ステータス更新)

[データ層]
prisma/schema.prisma   — Order / OrderItem モデル

[型定義層]
types/order.ts         — OrderStatus 等

依存関係のポイント:

  • フロントは API を通じてのみ DB にアクセスする(直接 Prisma は呼ばない)
  • 型定義は types/ で一元管理し、フロント・API の両方で共有する
  • UI コンポーネントは props だけに依存し、API や DB には依存しない

まとめ

  • request.json() は非同期関数なので await が必須。戻り値は any 型になるため、型定義を付けて型安全にする
  • NextResponse.json() の第1引数はボディ、第2引数は設定。ステータスコードはボディに入れない
  • ガード処理後のオプショナルチェーンは不要。例外処理(try/catch)と response.ok チェックを忘れずに

参考

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?