Next.js API Routes 完全ガイド:Route Handlers からエラー処理のベストプラクティスまで

先週の金曜日の午後、プロダクトマネージャーがやってきて言いました。「ユーザー登録 API を追加できる?」
私はプロジェクトの pages/api フォルダを開き、いつもの手順で書こうとしましたが、フォルダは空っぽでした。そこで思い出しました。これは App Router を使った新しいプロジェクトで、API の書き方が完全に変わっていたのです。
Next.js のドキュメントを開き、「Route Handlers」という言葉を見て、また新しい概念かと少し身構えました。午後一杯かけてドキュメントを読み、サンプルコードを動かして、ようやく route.ts とは何なのか、なぜ慣れ親しんだ req と res が使えなくなったのかを理解しました。
もしあなたが Next.js の新しいバックエンド API の書き方に戸惑っているなら、この記事が思考の整理に役立つはずです。Pages Router と App Router の API の書き方がどう変わったのかを比較し、リクエストの処理、レスポンスの設計、そして優雅なエラー処理の方法を実践的なケーススタディを通して解説します。心配しないでください。この記事を読み終わる頃には、Next.js で自信を持ってバックエンド API を書けるようになっているでしょう。
API Routes の基礎:2つの書き方の本質的な違い
Pages Router 時代の書き方
Next.js 13 以前は、pages/api フォルダに API を書いていました。当時の書き方は Express に似ていて、Node.js の req と res オブジェクトを使用していました。
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json({ message: 'Hello from Pages Router!' })
}この書き方の利点は、Node.js や Express の経験があればすぐに習得できることです。しかし欠点もありました。Node.js 固有の API に依存しているため、Edge Runtime などのエッジ環境へのデプロイ時に問題が発生することがありました。
App Router 時代の Route Handlers
Next.js 13 で App Router が導入され、API のアプローチが根本的に変わりました。app ディレクトリ内に route.ts ファイルを作成し、Web 標準の Request と Response API を使用します。
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: 'Hello from Route Handlers!' })
}初めてこの書き方を見たとき、私も違和感を覚えました。「なぜ req と res が使えないの?」と。しかし、変更には明確な理由があります。
- Web 標準への準拠: ブラウザネイティブの
RequestとResponseAPI を使用することで、コードの汎用性が高まり、現代の Web 開発のトレンドに沿っています。 - より良い型安全性: TypeScript のサポートが強化され、追加の型定義をインストールする必要が減りました。
- Edge Runtime のサポート: Vercel Edge や Cloudflare Workers などのエッジ環境にデプロイ可能になり、応答速度が向上します。
核心的な違いの比較
| 特性 | Pages Router | App Router |
|---|---|---|
| ファイル位置 | pages/api/* | app/*/route.ts |
| API 設計 | Node.js req/res | Web 標準 Request/Response |
| HTTP メソッド | デフォルトエクスポートで req.method を分岐 | 各メソッドを個別エクスポート (GET, POST 等) |
| キャッシュ動作 | キャッシュしない | GET 请求はデフォルトでキャッシュされる |
正直なところ、最初は変更の理由を理解できませんでしたが、実際に使ってみると、新しい書き方の方が明確だと気づきました。特に異なる HTTP メソッドを処理する場合、if (req.method === 'GET') のような分岐を書く必要がなくなりました。
Route Handlers 実戦:HTTP リクエストの作成と処理
サポートされる HTTP メソッド
Route Handlers は 7 つの HTTP メソッドをサポートしています:GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS。各メソッドは名前付きエクスポートに対応しており、この設計はとても気に入っています。コード構造を見るだけで、その API がどの操作をサポートしているかが一目瞭然だからです。
以下は完全なユーザー管理 API の例です:
// app/api/users/route.ts
// ユーザーリストの取得
export async function GET(request: Request) {
// URL からクエリパラメータを取得
const { searchParams } = new URL(request.url)
const page = searchParams.get('page') || '1'
return Response.json({
users: [
{ id: 1, name: '佐藤' },
{ id: 2, name: '鈴木' }
],
page: parseInt(page)
})
}
// 新規ユーザー作成
export async function POST(request: Request) {
// JSON リクエストボディを解析
const body = await request.json()
return Response.json({
id: 3,
name: body.name
}, { status: 201 })
}リクエストデータの処理
Next.js Route Handlers はリクエストデータを取得するためにいくつかの方法を提供しています。最初は混乱しましたが、整理すると以下のようになります。
- URL パラメータ:
request.urlとURLオブジェクトを使用
const { searchParams } = new URL(request.url)
const keyword = searchParams.get('q')- リクエストボディ (JSON):
await request.json()を使用
const body = await request.json()
console.log(body.email)- リクエストボディ (FormData):
await request.formData()を使用
const formData = await request.formData()
const file = formData.get('avatar')- ヘッダーと Cookie:
next/headersからインポート
import { headers, cookies } from 'next/headers'
export async function GET() {
const headersList = await headers()
const cookieStore = await cookies()
const token = headersList.get('authorization')
const userId = cookieStore.get('user_id')
}注意: Next.js 15 以降、headers と cookies は非同期関数になりました。
- 動的ルートパラメータ: 関数の第2引数から取得
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
// Next.js 15 以降 params は非同期になる可能性があるため、必要に応じて await
const userId = params.id
return Response.json({ userId })
}レスポンスの構築
JSON を返すのが最も一般的なシナリオで、Response.json() を使用します。
export async function GET() {
return Response.json({
success: true,
data: { message: '操作成功' }
})
}カスタムステータスコードやヘッダーの設定も簡単です:
export async function POST(request: Request) {
const body = await request.json()
if (!body.email) {
return Response.json(
{ error: 'メールアドレスは必須です' },
{ status: 400 }
)
}
return Response.json(
{ id: 123, email: body.email },
{
status: 201,
headers: {
'X-Request-Id': 'abc-123',
'Cache-Control': 'no-cache'
}
}
)
}リアルワールドシナリオ:ユーザー登録 API
これまでの知識を組み合わせて、完全なユーザー登録 API を書いてみましょう。
// app/api/auth/register/route.ts
import { headers } from 'next/headers'
export async function POST(request: Request) {
const headersList = await headers()
const contentType = headersList.get('content-type')
if (!contentType?.includes('application/json')) {
return Response.json(
{ error: 'JSON形式で送信してください' },
{ status: 400 }
)
}
const body = await request.json()
const { username, email, password } = body
if (!username || !email || !password) {
return Response.json(
{ error: 'すべてのフィールドを入力してください' },
{ status: 400 }
)
}
// ここでデータベースにユーザーを保存する処理
// const user = await db.user.create({ username, email, password })
return Response.json({
success: true,
data: {
id: 1,
username,
email
}
}, { status: 201 })
}エラー処理のベストプラクティス
Try-Catch の正しい使い方
API を書き始めた頃、私はすべてのロジックを巨大な try-catch で囲んでいました。
// ❌ 非推奨: すべてのエラーをまとめてキャッチ
export async function POST(request: Request) {
try {
const body = await request.json()
// 複雑なビジネスロジック...
return Response.json({ success: true })
} catch (error) {
return Response.json({ error: '操作失敗' }, { status: 500 })
}
}これでは全てのエラーが 500 になってしまい、デバッグが困難です。操作ごとにエラー処理を分けることをお勧めします。
// ✅ 推奨: エラーの種類を区別する
export async function POST(request: Request) {
let body
try {
body = await request.json()
} catch (error) {
return Response.json(
{ error: 'JSON形式が不正です' },
{ status: 400 }
)
}
if (!body.email) {
return Response.json(
{ error: 'メールアドレスは必須です' },
{ status: 400 }
)
}
try {
const user = await db.user.create(body)
return Response.json({ success: true, data: user })
} catch (error: any) {
if (error.code === 'P2002') { // Prisma のユニーク制約違反
return Response.json(
{ error: 'このメールアドレスは既に登録されています' },
{ status: 409 }
)
}
console.error('Database error:', error)
return Response.json(
{ error: 'サーバー内部エラー' },
{ status: 500 }
)
}
}構造化されたエラーレスポンス
エラーレスポンスの形式がバラバラだと、フロントエンドの実装が大変になります。統一されたフォーマットを定義しましょう。
interface ErrorResponse {
success: false
error: string
code?: string
details?: any
requestId?: string
}
interface SuccessResponse<T> {
success: true
data: T
requestId?: string
}ヘルパー関数ライブラリを作ると便利です。
// lib/api-response.ts
import { nanoid } from 'nanoid'
export function successResponse<T>(data: T, status: number = 200) {
return Response.json({
success: true,
data,
requestId: nanoid()
}, { status })
}
export function errorResponse(
error: string,
status: number = 500,
code?: string,
details?: any
) {
const isDev = process.env.NODE_ENV === 'development'
return Response.json({
success: false,
error,
code,
details: isDev ? details : undefined, // 本番環境では詳細を隠す
requestId: nanoid()
}, { status })
}レスポンス形式設計
RESTful の原則に従い、URL でリソースを表現し、HTTP メソッドで操作を表現し、ステータスコードで結果を伝えるのが基本です。また、TypeScript を活用して型定義を共有すると、フロントエンドの実装が非常に楽になります。
types/api.ts のようなファイルでリクエストとレスポンスの型を定義し、クライアントとサーバーで共有することを強く推奨します。
よくある問題と解決策
GET リクエストのデフォルトキャッシュ問題
私が最もハマったのがこれです。GET リクエストの結果がキャッシュされ、データ更新が反映されない現象です。Next.js の App Router は、静的と判断できる GET リクエストを積極的にキャッシュします。
解決策は、動的であることを明示することです:
export const dynamic = 'force-dynamic'を追加するexport const revalidate = 0を設定するRequestオブジェクトを使用する(クエリパラメータや Cookie の読み取りなど)
// app/api/users/route.ts
export const dynamic = 'force-dynamic' // キャッシュ無効化
export async function GET() {
const users = await db.user.findMany()
return Response.json({ success: true, data: users })
}デプロイ後の 404 エラー
ローカルでは動くのにデプロイすると 404 になる場合、以下を確認してください:
- ファイル名が
route.tsまたはroute.jsであること。api.tsは不可。 page.tsxと同じディレクトリにroute.tsを置かないこと(競合します)。- ファイルが git にコミットされているか。
try-catch 内での Redirect
Next.js の redirect() 関数は内部で特別なエラーを投げることで動作します。これを try-catch ブロック内で呼ぶと、redirect が catch されてしまい、正常に機能しません。
// ❌ 間違い
try {
if (!auth) redirect('/login')
} catch (e) {
// ここに来てしまう
}
// ✅ 正解
if (!auth) redirect('/login')
try {
// ...
} catch (e) {
// ...
}ただし、Route Handlers では通常 redirect よりも 401 ステータスを返す方が適切です。
結論
Route Handlers は、Next.js でバックエンド API を構築するための強力で標準に準拠した方法です。最初は変更に戸惑うかもしれませんが、Web 標準への回帰、型安全性の向上、エッジコンピューティングへの対応など、メリットは大きいです。
この記事で紹介したベストプラクティスやエラー処理パターンを活用して、より堅牢で保守性の高い API を構築してください。
FAQ
Route Handlers と Pages Router API Routes の主な違いは何ですか?
GET リクエストが最新のデータを返さないのはなぜですか?
いつ Route Handlers を使用し、いつ Server Components を使用すべきですか?
API エラーをどのように優雅に処理すべきですか?
デプロイ後、API が 404 になるのはなぜですか?
4 min read · 公開日: 2026年1月5日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


コメント
GitHubアカウントでログインしてコメントできます