ウェブエンジニア問題集
第8章

Route HandlersとAPI Route — route.tsでAPIエンドポイントを作る

17
この章の目次開く

ここまでは、Server Componentから外部APIやデータソースを fetch して画面に表示する方法を学びました。次は、Next.jsアプリ自身がAPIエンドポイントを提供する側になる方法です。

App Routerでは、app/api/.../route.ts に関数を書くことで、JSONを返すAPIを作れます。この仕組みを Route Handlers と呼びます。Pages Router時代の名前に慣れている人は、API RoutesのApp Router版と考えると理解しやすいです。

学習者学習者

Next.jsって画面を作るフレームワークだと思ってたけど、APIも作れるの? Expressみたいに別サーバーを立てる必要はないの?

はい。小〜中規模のWebアプリなら、Next.jsの中にAPIを作るだけで十分な場面が多いです。画面、データ取得、API、フォーム更新を同じプロジェクトで扱えるのが、Next.jsをフルスタックフレームワークと呼ぶ理由の1つです。

Next.jsでAPIエンドポイントを実装しているイメージ


Route Handlersとは

Route Handlerは、特定のURLに対するHTTPリクエストを受け取り、レスポンスを返す関数です。page.tsx が「画面」を返すのに対して、route.ts は「データ」や「ステータス」を返します。

src/app/
├── users/
│   └── page.tsx        → /users の画面
└── api/
    └── users/
        └── route.ts    → /api/users のAPI

app/api/users/route.ts を作ると、/api/users というURLでアクセスできるAPIになります。

// src/app/api/users/route.ts
export async function GET() {
  return Response.json([
    { id: 1, name: '田中' },
    { id: 2, name: '佐藤' },
  ]);
}
ts

ブラウザや fetch('/api/users') からアクセスすると、JSONが返ります。


page.tsxとroute.tsの違い

ファイル返すもの使い道
page.tsxReactコンポーネントユーザーが見る画面
route.tsResponse / NextResponseJSON、Webhook、外部連携用API

たとえば、ユーザー一覧画面は page.tsx、その画面が使うユーザー一覧APIは route.ts にします。

src/app/
├── users/
│   └── page.tsx
└── api/
    └── users/
        └── route.ts
// src/app/users/page.tsx
export default async function UsersPage() {
  const res = await fetch('http://localhost:3000/api/users');
  const users = await res.json();
 
  return (
    <ul>
      {users.map((user: { id: number; name: string }) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
tsx

HTTPメソッドで処理を分ける

Route Handlerでは、HTTPメソッド名と同じ名前の関数を export します。

// src/app/api/users/route.ts
export async function GET() {
  // 一覧取得
}
 
export async function POST(request: Request) {
  // 新規作成
}
 
export async function PUT(request: Request) {
  // 更新
}
 
export async function DELETE(request: Request) {
  // 削除
}
ts

同じ /api/users というURLでも、リクエストのメソッドによって呼ばれる関数が変わります。

メソッド主な意味
GET取得ユーザー一覧を取得する
POST作成新しいユーザーを作る
PUT全体更新・置き換えユーザー情報を更新する
PATCH部分更新名前だけ変更する
DELETE削除ユーザーを削除する
学習者学習者

同じURLなのに、GETPOST で別の処理になるんだね。URLだけじゃなくて、HTTPメソッドもルーティングの条件になるのか。

定義していないメソッドでアクセスされた場合、そのメソッドは許可されません。まずは GETPOST から始め、必要に応じて PUT / PATCH / DELETE を追加するとよいです。


GET — データを取得する

GET は、一覧や詳細を取得するためのメソッドです。リクエストボディは基本的に使わず、クエリパラメータや動的ルートで条件を受け取ります。

// src/app/api/users/route.ts
const users = [
  { id: 1, name: '田中', role: 'admin' },
  { id: 2, name: '佐藤', role: 'member' },
];
 
export async function GET() {
  return Response.json({ users });
}
ts

クエリパラメータを読む

/api/users?role=admin のようなクエリは、request.url から URL オブジェクトを作って読み取れます。

// src/app/api/users/route.ts
import type { NextRequest } from 'next/server';
 
export async function GET(request: NextRequest) {
  const role = request.nextUrl.searchParams.get('role');
 
  const filteredUsers = role ? users.filter((user) => user.role === role) : users;
 
  return Response.json({ users: filteredUsers });
}
ts

NextRequest を使うと、通常の Request よりもNext.js向けの便利な情報を扱えます。クエリを読むなら request.nextUrl.searchParams が分かりやすいです。

const page = request.nextUrl.searchParams.get('page') ?? '1';
const keyword = request.nextUrl.searchParams.get('q') ?? '';
ts

POST — JSON bodyを受け取って作成する

POST は、新しいデータを作成するときによく使います。送られてきたJSONは await request.json() で読み取ります。

// src/app/api/users/route.ts
import { NextResponse } from 'next/server';
 
export async function POST(request: Request) {
  const body = await request.json();
 
  const name = String(body.name ?? '').trim();
  const role = String(body.role ?? 'member').trim();
 
  if (!name) {
    return NextResponse.json({ error: 'name is required' }, { status: 400 });
  }
 
  const user = {
    id: Date.now(),
    name,
    role,
  };
 
  // 実務ではここでDBに保存する
  return NextResponse.json({ user }, { status: 201 });
}
ts

クライアント側からは次のように呼び出します。

await fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: '山田',
    role: 'member',
  }),
});
ts

request.json()は一度だけ読む

リクエストボディはストリームなので、基本的に一度しか読めません。

const body = await request.json();
 
// 同じrequestに対してもう一度 json() を呼ぶ設計は避ける
// const body2 = await request.json();
ts

読み取った body を変数に入れて、以降の処理で使い回しましょう。

必ず入力値を検証する

APIに届く値は信用できません。ブラウザ側でバリデーションしていても、curlやPostmanから直接APIを叩けます。

function isValidRole(value: unknown): value is 'admin' | 'member' {
  return value === 'admin' || value === 'member';
}
 
export async function POST(request: Request) {
  const body = await request.json();
 
  if (typeof body.name !== 'string' || body.name.trim() === '') {
    return NextResponse.json({ error: 'name is required' }, { status: 400 });
  }
 
  if (!isValidRole(body.role)) {
    return NextResponse.json({ error: 'role is invalid' }, { status: 400 });
  }
 
  return NextResponse.json(
    { user: { id: Date.now(), name: body.name, role: body.role } },
    { status: 201 },
  );
}
ts

実務では、Zodなどのバリデーションライブラリを使うと、型チェックとエラーメッセージの管理がしやすくなります。


PUT — 既存データを更新する

PUT は、既存データを更新するときに使います。どのデータを更新するかは、動的ルートで受け取るのが一般的です。

src/app/api/users/[id]/route.ts

このファイルは /api/users/123 のようなURLに対応します。

// src/app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';
 
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const body = await request.json();
 
  const name = String(body.name ?? '').trim();
 
  if (!name) {
    return NextResponse.json({ error: 'name is required' }, { status: 400 });
  }
 
  // 実務ではここでDBを更新する
  const updatedUser = {
    id: Number(id),
    name,
  };
 
  return NextResponse.json({ user: updatedUser });
}
ts

App Routerでは、動的セグメントの paramsawait して取り出します。[id] というフォルダ名にしたので、params には { id: string } が入ります。

const { id } = await params;
ts

クライアント側からは、更新対象のIDをURLに含めて呼びます。

await fetch('/api/users/123', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: '山田太郎',
  }),
});
ts

DELETE — 既存データを削除する

DELETE は、指定したデータを削除するときに使います。こちらも動的ルートでIDを受け取ることが多いです。

// src/app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';
 
export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
 
  if (!id) {
    return NextResponse.json({ error: 'id is required' }, { status: 400 });
  }
 
  // 実務ではここでDBから削除する
  return new Response(null, { status: 204 });
}
ts

削除に成功し、返すJSONがない場合は 204 No Content を返すことがあります。JSONで結果を返したいなら、次のようにしても構いません。

return NextResponse.json({ ok: true });
ts

クライアント側からは次のように呼び出します。

await fetch('/api/users/123', {
  method: 'DELETE',
});
ts

削除は取り消しが難しい操作なので、実務ではサーバー側で認可チェックを必ず行います。


NextRequest — リクエストを扱う

NextRequest は、標準のWeb Request をNext.js向けに拡張した型です。通常の request.json()request.headers.get() に加えて、nextUrlcookies を扱いやすくなります。

先生先生

迷ったら、まずは標準の Request で書いてみよう。クエリ、Cookie、Next.js固有のURL情報を扱いたくなったら NextRequest に切り替える、くらいで十分だよ。

import type { NextRequest } from 'next/server';
 
export async function GET(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  const page = request.nextUrl.searchParams.get('page') ?? '1';
  const token = request.cookies.get('token')?.value;
  const userAgent = request.headers.get('user-agent');
 
  return Response.json({
    pathname,
    page,
    hasToken: Boolean(token),
    userAgent,
  });
}
ts

よく使う読み取りは次の通りです。

書き方取れるもの
request.nextUrl.pathnameパス名
request.nextUrl.searchParams.get('q')クエリパラメータ
request.cookies.get('token')?.valueCookie
request.headers.get('authorization')HTTPヘッダー
await request.json()JSON body
await request.formData()フォームデータ

NextRequest は必須ではありません。JSON bodyを読むだけなら Request で十分です。

export async function POST(request: Request) {
  const body = await request.json();
  return Response.json(body);
}
ts

クエリやCookieなどNext.js固有の便利機能を使いたいときに NextRequest を選ぶ、と覚えておきましょう。


NextResponse — レスポンスを返す

NextResponse は、標準のWeb Response をNext.js向けに拡張したものです。JSONを返す、Cookieを設定する、リダイレクトする、といった処理を読みやすく書けます。

標準の fetchRequestResponse の関係はJavaScriptのWeb API章で整理しています。Route Handlerでは、その標準Web APIをNext.js上で使っていると考えると理解しやすくなります。

APIレスポンスの内容を確認しているイメージ
import { NextResponse } from 'next/server';
 
export async function GET() {
  return NextResponse.json({
    message: 'Hello from Route Handler',
  });
}
ts

ステータスコードを付ける場合は、第2引数に指定します。

return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
ts

ヘッダーも同時に指定できます。

return NextResponse.json(
  { ok: true },
  {
    status: 200,
    headers: {
      'Cache-Control': 'no-store',
    },
  },
);
ts

Cookieを設定する

ログイン処理などでは、レスポンスにCookieを付けたいことがあります。

import { NextResponse } from 'next/server';
 
export async function POST() {
  const response = NextResponse.json({ ok: true });
 
  response.cookies.set('token', 'example-token', {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
  });
 
  return response;
}
ts

httpOnly: true にすると、ブラウザのJavaScriptからCookieを読めなくなります。認証トークンのような重要な値をCookieに入れる場合は、基本的に httpOnly を使います。

リダイレクトする

API内で条件に応じて別URLへリダイレクトしたい場合は、NextResponse.redirect() を使えます。

import { NextResponse } from 'next/server';
 
export async function GET(request: Request) {
  const url = new URL('/login', request.url);
  return NextResponse.redirect(url);
}
ts

エラーレスポンスの基本

APIでは、成功時だけでなく失敗時の返し方も重要です。ステータスコードとJSONの形を揃えておくと、クライアント側で扱いやすくなります。

ステータスコード自体の意味や、400401403404500 の使い分けは、HTTPステータスコード一覧で整理しています。Route HandlerでAPIを作る前に、返すべきコードの基準を押さえておくと設計がぶれません。

import { NextResponse } from 'next/server';
 
function errorResponse(message: string, status: number) {
  return NextResponse.json({ error: message }, { status });
}
 
export async function POST(request: Request) {
  try {
    const body = await request.json();
 
    if (typeof body.name !== 'string') {
      return errorResponse('name must be a string', 400);
    }
 
    return NextResponse.json({ ok: true }, { status: 201 });
  } catch {
    return errorResponse('invalid json body', 400);
  }
}
ts

よく使うステータスコードは次の通りです。

ステータス意味使う場面
200OK取得・更新成功
201Created作成成功
204No Content削除成功、返す本文なし
400Bad Request入力値が不正
401Unauthorizedログインが必要
403Forbidden権限がない
404Not Found対象データがない
409Conflict重複や競合
500Internal Server Error予期しないサーバーエラー

DB連携の基本パターン

実務では、Route Handlerの中でDBを直接操作するより、サービス関数に切り出すとテストしやすくなります。

src/
├── app/
│   └── api/
│       └── users/
│           └── route.ts
└── server/
    └── users.ts
// src/server/users.ts
export async function getUsers() {
  // Prismaやmysql2などでDBから取得する
  return [
    { id: 1, name: '田中' },
    { id: 2, name: '佐藤' },
  ];
}
 
export async function createUser(input: { name: string }) {
  // DBへ保存する
  return { id: Date.now(), name: input.name };
}
ts
// src/app/api/users/route.ts
import { NextResponse } from 'next/server';
import { createUser, getUsers } from '@/server/users';
 
export async function GET() {
  const users = await getUsers();
  return NextResponse.json({ users });
}
 
export async function POST(request: Request) {
  const body = await request.json();
 
  if (typeof body.name !== 'string' || body.name.trim() === '') {
    return NextResponse.json({ error: 'name is required' }, { status: 400 });
  }
 
  const user = await createUser({ name: body.name.trim() });
  return NextResponse.json({ user }, { status: 201 });
}
ts

Route Handlerは「HTTPの入口」に集中させ、実際の業務ロジックは別ファイルに置く。これが保守しやすい構成です。


認証・認可はサーバー側で必ず確認する

Route Handlerは外部から直接アクセスできる入口です。画面側でボタンを隠していても、API自体は直接叩けます。

import { NextResponse } from 'next/server';
 
async function getCurrentUser() {
  // 実務ではセッションやJWTからログインユーザーを取得する
  return { id: 1, role: 'member' };
}
 
export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) {
  const currentUser = await getCurrentUser();
 
  if (currentUser.role !== 'admin') {
    return NextResponse.json({ error: 'forbidden' }, { status: 403 });
  }
 
  const { id } = await params;
  // 管理者だけ削除できる
  return NextResponse.json({ deletedId: id });
}
ts

入力値の検証は「値が正しいか」、認可は「その操作をしてよい人か」を確認する処理です。どちらもクライアント側だけで済ませてはいけません。


Route HandlersとServer Actionsの使い分け

Next.jsでは、データ更新にServer Actionsも使えます。では、Route HandlerとServer Actionsはどう使い分ければよいのでしょうか。

使いたい場面向いているもの
外部サービスからWebhookを受けるRoute Handler
モバイルアプリや別フロントエンドから呼ぶRoute Handler
REST APIとしてURLを公開したいRoute Handler
<form> 送信でDBを更新したいServer Actions
同じNext.jsアプリ内だけで完結する更新Server Actions
APIのURL設計を省きたいServer Actions

Route Handlerは「HTTP APIを明示的に作る」仕組みです。Server Actionsは「Next.jsアプリ内からサーバー関数を呼ぶ」仕組みです。

先生先生

外から呼ばれる入口が必要ならRoute Handler。フォーム送信や画面内の更新だけならServer Actions。この基準で考えると迷いにくいよ。


よくあるハマりどころ

ハマりどころ原因対策
/api/users が404になるapp/api/users/route.ts ではなく別の場所に置いているapp 配下のURL構造を確認する
JSON bodyが読めないContent-Type がない、またはbodyがJSONでないheadersJSON.stringify を確認する
params.id が取れないApp Routerでは paramsawait していないconst { id } = await params にする
エラー時も200で返しているステータスコードを指定していないNextResponse.json(..., { status: 400 }) を使う
削除APIを誰でも叩ける認可チェックがないサーバー側でログイン状態と権限を確認する

まとめ

  • App Routerでは app/api/.../route.ts でAPIエンドポイントを作る
  • page.tsx は画面、route.ts はJSONやステータスを返すAPI
  • GET / POST / PUT / DELETE など、HTTPメソッド名の関数を export する
  • NextRequest はクエリ、Cookie、ヘッダーなどを読みやすくするリクエスト型
  • NextResponse はJSON、ステータスコード、Cookie、リダイレクトを返しやすくするレスポンス型
  • 入力値のバリデーションと認証・認可は、必ずRoute Handler側で行う
  • 外部から呼ばれるHTTP APIはRoute Handler、同じNext.jsアプリ内のフォーム更新はServer Actionsが向いている

次の章では、リクエストがページやAPIに届く前に処理を挟む ProxyとMiddleware を扱います。ログイン判定、リダイレクト、middleware.ts から proxy.ts への変更を理解しましょう。

Next.jsクイズに挑戦するこの章で学んだNext.jsの知識を、4択クイズでアウトプットして定着させよう