Next.js: API ルート & ルートハンドラ
最終更新:2026-08-26
ルートハンドラは Next.js の API レイヤーを形成します — サーバーアクションでは不十分な場合、標準的な RESTful エンドポイントがモダン Web の礎石であり続けます。
1. 学習目標
route.tsファイルによる GET/POST/PUT/DELETE エンドポイントの定義- Zod によるリクエストボディとクエリパラメータの検証
NextResponseレスポンス形式 (json/redirect/rewrite)middleware.tsによる matcher 設定とリクエスト書き換え- CORS クロスオリジン設定とレート制限の実装
2. ある DevOps エンジニアの実話
(1) 課題: チームが認証ロジックを5ページで8回実装してしまった
TaskFlow のコードをレビュー中、Diana はすべての API ルートが同じ15行の認証コードで始まっていることに気づきました — Cookie からセッションを解析し、トークンを検証し、401 レスポンスを返します。8つの API エンドポイント × 15行 = 120行の重複コードです。さらに悪いことに、3つのエンドポイントには認証チェックがなく、未認証ユーザーからのリクエストにユーザーデータを直接露出させていました。
| 問題 | データ |
|---|---|
| 重複した認証コード | 120行 (8エンドポイント × 15行) |
| 保護されていないエンドポイント | 3 |
| セキュリティ監査不合格 | 2回 |
| 修復時間 | 1回あたり4時間 |
(2) ミドルウェアによる解決策
middleware.tsを使用して認証、ログ記録、CORS を一元管理します — 単一のファイルですべてのリクエストのライフサイクル全体を制御します。
// middleware.ts — シングルサインオン + ログ
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('session-token')?.value
// API リクエストの認証を検査
if (request.nextUrl.pathname.startsWith('/api/')) {
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
}
// セキュリティヘッダーを追加
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
return response
}
export const config = {
matcher: '/api/:path*',
}
(3) 成果
| 指標 | 導入前 (ミドルウェアなし) | 導入後 (ミドルウェアあり) |
|---|---|---|
| 認証コード | 120行 (分散) | 15行 (集約) |
| 保護されていないエンドポイント | 3 | 0 |
| セキュリティ監査 | ❌ 不合格 | ✅ 合格 |
| 新規エンドポイントの作業量 | 認証15行 + ロジック | ロジックのみ |
3. ルートハンドラの基本
ルートハンドラは app/api/**/route.ts ファイルで定義され、各 HTTP メソッドに対応するエクスポート非同期関数を持ちます:
| HTTP メソッド | エクスポート関数 | ルートファイル |
|---|---|---|
| GET | export async function GET() |
app/api/items/route.ts |
| POST | export async function POST() |
app/api/items/route.ts |
| PUT | export async function PUT() |
app/api/items/[id]/route.ts |
| DELETE | export async function DELETE() |
app/api/items/[id]/route.ts |
| PATCH | export async function PATCH() |
app/api/items/[id]/route.ts |
(1) 基本的な GET エンドポイント
// app/api/items/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const items = await db.item.findMany()
return NextResponse.json(items)
}
(2) 動的ルーティングパラメータ
// app/api/items/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const item = await db.item.findUnique({ where: { id: Number(params.id) } })
if (!item) {
return NextResponse.json({ error: 'アイテムが見つかりません' }, { status: 404 })
}
return NextResponse.json(item)
}
(3) POST でリソースを作成
// app/api/items/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const body = await request.json()
const item = await db.item.create({ data: body })
return NextResponse.json(item, { status: 201 })
}
▶ サンプル: 完全な CRUD ルートハンドラ (難易度: ⭐⭐)
POST app/api/items/route.ts → 新しいリソースを作成し、作成されたアイテムを 201 ステータスで返します。
// app/api/todos/route.ts — GET + POST
import { NextRequest, NextResponse } from 'next/server'
const API = 'https://jsonplaceholder.typicode.com'
export async function GET() {
const todos = await fetch(`${API}/todos?_limit=5`).then(r => r.json())
return NextResponse.json(todos)
}
export async function POST(request: NextRequest) {
const body = await request.json()
const todo = await fetch(`${API}/todos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json())
return NextResponse.json(todo, { status: 201 })
}
GET app/api/todos/route.ts → JSON データを 200 ステータスで返します。
POST app/api/todos/route.ts → 新しいリソースを作成し、作成されたアイテムを 201 ステータスで返します。
// app/api/todos/[id]/route.ts — GET + PUT + DELETE
import { NextRequest, NextResponse } from 'next/server'
const API = 'https://jsonplaceholder.typicode.com'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const todo = await fetch(`${API}/todos/${params.id}`).then(r => r.json())
if (!todo || todo.id === undefined) {
return NextResponse.json({ error: '見つかりません' }, { status: 404 })
}
return NextResponse.json(todo)
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const updated = await fetch(`${API}/todos/${params.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json())
return NextResponse.json(updated)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await fetch(`${API}/todos/${params.id}`, { method: 'DELETE' })
return NextResponse.json({ success: true })
}
4. Zod リクエスト検証
REST API でのリクエストデータの検証にも Zod を推奨します — 手動の if チェックよりも簡潔で型安全です。
(1) リクエストボディ検証
// app/api/items/route.ts — Zod による POST リクエストボディ検証
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const createItemSchema = z.object({
name: z.string().min(2).max(100),
price: z.number().positive('価格は正の数である必要があります'),
category: z.enum(['electronics', 'clothing', 'food']),
tags: z.array(z.string()).max(5).optional(),
})
export async function POST(request: NextRequest) {
const body = await request.json()
const validated = createItemSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: '検証に失敗しました', details: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const item = await db.item.create({ data: validated.data })
return NextResponse.json(item, { status: 201 })
}
(2) クエリパラメータ検証
// app/api/items/route.ts — Zod によるクエリパラメータ検証
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const querySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(10),
search: z.string().optional(),
})
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl
const query = querySchema.safeParse({
page: searchParams.get('page'),
limit: searchParams.get('limit'),
search: searchParams.get('search'),
})
if (!query.success) {
return NextResponse.json(
{ error: '無効なクエリパラメータ', details: query.error.flatten().fieldErrors },
{ status: 400 }
)
}
const { page, limit, search } = query.data
const items = await db.item.findMany({
skip: (page - 1) * limit,
take: limit,
where: search ? { name: { contains: search } } : undefined,
})
return NextResponse.json({ items, page, limit })
}
▶ サンプル: Zod 検証の完全な API (難易度: ⭐⭐)
GET app/api/items/route.ts → Zod でクエリパラメータを検証し、フィルタリングされた結果を JSON で返します。
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { revalidateTag } from 'next/cache'
const userSchema = z.object({
name: z.string().min(2, '名前が短すぎます'),
email: z.string().email('メールアドレスが無効です'),
role: z.enum(['admin', 'user', 'viewer']).default('user'),
})
export async function POST(request: NextRequest) {
const body = await request.json()
const validated = userSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: '検証に失敗しました', fields: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const user = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validated.data),
}).then(r => r.json())
revalidateTag('users')
return NextResponse.json(user, { status: 201 })
}
POST app/api/users/route.ts → Zod でリクエストボディを検証し、リソースを作成、201 を返します。
5. NextResponse レスポンス形式
NextResponse は Next.js が提供する専用のレスポンスツールで、複数のレスポンス形式をサポートします:
| メソッド | 目的 | 例 |
|---|---|---|
NextResponse.json(data, opts?) |
JSON レスポンス | NextResponse.json({ id: 1 }, { status: 201 }) |
NextResponse.redirect(url) |
リダイレクト | NextResponse.redirect(new URL('/login', request.url)) |
NextResponse.next() |
ミドルウェアチェーンを継続 | return NextResponse.next() |
NextResponse.rewrite(url) |
サーバーサイド URL 書き換え | NextResponse.rewrite(new URL('/fallback', request.url)) |
(1) JSON レスポンス
// app/api/auth/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ status: 'healthy', uptime: process.uptime() })
}
(2) リダイレクト
// app/api/redirect/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const target = request.nextUrl.searchParams.get('to') ?? '/'
return NextResponse.redirect(new URL(target, request.url))
}
(3) リライト (URL 書き換え)
// middleware.ts — /products/old-slug を新しいパスに書き換え
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/products/old-')) {
const newPath = request.nextUrl.pathname.replace('/old-', '/')
return NextResponse.rewrite(new URL(newPath, request.url))
}
return NextResponse.next()
}
▶ サンプル: レスポンス形式の比較 (難易度: ⭐)
ミドルウェアがリクエストを傍受し、条件に基づいて URL を書き換えます。
// app/api/response-demo/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const format = request.nextUrl.searchParams.get('format') ?? 'json'
switch (format) {
case 'json':
return NextResponse.json({ message: 'Hello', timestamp: Date.now() })
case 'redirect':
return NextResponse.redirect(new URL('/api/response-demo?format=json', request.url))
case 'rewrite':
return NextResponse.rewrite(new URL('/api/response-demo?format=json', request.url))
default:
return NextResponse.json({ error: '不明な形式' }, { status: 400 })
}
}
GET app/api/response-demo/route.ts → JSON データを 200 ステータスで返します。
6. ミドルウェア
middleware.ts は Next.js のリクエストインターセプターです — 各リクエストがページや API に到達する前に実行され、パスマッチング、リクエスト書き換え、ヘッダー注入、認証チェックをサポートします。
sequenceDiagram
participant Client as ブラウザ
participant MW as middleware.ts
participant Route as ルートハンドラ/ページ
Client->>MW: リクエスト /api/todos
MW->>MW: matcher ルールにマッチ
MW->>MW: 認証検査 / ヘッダー注入
alt 通過
MW->>Route: NextResponse.next()
Route-->>Client: 通常レスポンス
else 未認証
MW-->>Client: NextResponse.json(401)
else リダイレクト
MW-->>Client: NextResponse.redirect(/login)
end
| 設定 | 型 | 説明 |
|---|---|---|
matcher |
string[] | パスパターンのマッチング (glob 対応) |
request.nextUrl |
URL | 現在のリクエストの URL オブジェクト |
request.cookies |
Map | リクエスト Cookie |
request.headers |
Headers | リクエストヘッダー |
NextResponse.next() |
Response | 通常通り処理を継続 |
NextResponse.redirect() |
Response | 別の URL にリダイレクト |
(1) Matcher 設定
// middleware.ts — matcher レイアウト
export const config = {
matcher: [
'/api/:path*', // すべての API ルーティング
'/dashboard/:path*', // すべての dashboard ページ
'/((?!_next|static|favicon.ico).*)', // 静的リソースを除外
],
}
(2) よくあるミドルウェアパターン
// middleware.ts — よくあるパターンの概要
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 1. 認証検査
const token = request.cookies.get('session')?.value
const isAuthPage = pathname.startsWith('/login') || pathname.startsWith('/register')
if (!token && !isAuthPage && pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 2. セキュリティヘッダー
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
// 3. リクエスト ID を追加 (追跡)
const requestId = crypto.randomUUID()
response.headers.set('X-Request-Id', requestId)
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}
▶ サンプル: ミドルウェアログ記録 (難易度: ⭐)
ミドルウェアがリクエストを傍受し、条件に基づいてリダイレクトします。
// middleware.ts — リクエストログ
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const start = Date.now()
const { method, nextUrl } = request
console.log(`[${new Date().toISOString()}] ${method} ${nextUrl.pathname}`)
const response = NextResponse.next()
// 非同期レスポンス時間ログ記録
response.headers.set('X-Response-Time', `${Date.now() - start}ms`)
return response
}
export const config = {
matcher: '/api/:path*',
}
[${new Date(
7. CORS とレート制限
(1) CORS 設定
// app/api/cors-config/route.ts — 単一エンドポイントの CORS
import { NextRequest, NextResponse } from 'next/server'
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://your-app.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
}
export async function OPTIONS() {
return NextResponse.json({}, { headers: corsHeaders })
}
export async function GET() {
return NextResponse.json(
{ data: 'CORS enabled' },
{ headers: corsHeaders }
)
}
(2) グローバル CORS ミドルウェア
// middleware.ts — グローバル CORS
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/')) {
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
// プリフライトリクエストは即時返却
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: response.headers })
}
return response
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*',
}
(3) レート制限
// lib/rate-limit.ts — シンプルなメモリレート制限
const rateMap = new Map<string, { count: number; resetAt: number }>()
export function rateLimit(key: string, maxRequests: number, windowMs: number): boolean {
const now = Date.now()
const record = rateMap.get(key)
if (!record || now > record.resetAt) {
rateMap.set(key, { count: 1, resetAt: now + windowMs })
return true // 許可
}
if (record.count >= maxRequests) {
return false // 制限
}
record.count++
return true
}
// app/api/rate-limited/route.ts — レート制限の使用
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
export async function GET(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
const allowed = rateLimit(ip, 10, 60_000) // 1分あたり10回
if (!allowed) {
return NextResponse.json(
{ error: 'リクエストが多すぎます' },
{ status: 429, headers: { 'Retry-After': '60' } }
)
}
return NextResponse.json({ message: 'Success', timestamp: Date.now() })
}
▶ サンプル: 完全な CORS + レート制限 (難易度: ⭐⭐⭐)
GET app/api/rate-limited/route.ts → データを JSON で返します。悪用を防ぐためのレート制限付き。
// app/api/secure/route.ts — CORS + レート制限 + 認証
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(2).max(200),
content: z.string().min(10),
})
export async function POST(request: NextRequest) {
// 1. レート制限
const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
if (!rateLimit(ip, 20, 60_000)) {
return NextResponse.json({ error: 'レート制限を超えました' }, { status: 429 })
}
// 2. 認証チェック
const auth = request.headers.get('authorization')
if (!auth?.startsWith('Bearer ') || auth.slice(7) !== process.env.API_KEY) {
return NextResponse.json({ error: '認証されていません' }, { status: 401 })
}
// 3. 検証
const body = await request.json()
const validated = postSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: '検証に失敗しました', details: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
// 4. 処理
const post = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify({ ...validated.data, userId: 1 }),
}).then(r => r.json())
return NextResponse.json(post, { status: 201, headers: {
'Access-Control-Allow-Origin': '*',
}})
}
export async function OPTIONS() {
return NextResponse.json({}, { headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}})
}
POST app/api/secure/route.ts → Zod でリクエストボディを検証し、リソースを作成、201 を返します。
8. 完全な例: RESTful API サービス
// app/api/posts/route.ts — 投稿 CRUD API
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { revalidateTag } from 'next/cache'
// ======== スキーマ ========
const createPostSchema = z.object({
title: z.string().min(2).max(200),
body: z.string().min(10).max(5000),
userId: z.number().int().positive(),
})
const querySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(50).default(10),
})
const API = 'https://jsonplaceholder.typicode.com'
// ======== GET /api/posts?page=1&limit=10 ========
export async function GET(request: NextRequest) {
const query = querySchema.safeParse({
page: request.nextUrl.searchParams.get('page'),
limit: request.nextUrl.searchParams.get('limit'),
})
if (!query.success) {
return NextResponse.json(
{ error: '無効なクエリ', details: query.error.flatten().fieldErrors },
{ status: 400 }
)
}
const { page, limit } = query.data
const posts = await fetch(`${API}/posts?_page=${page}&_limit=${limit}`).then(r => r.json())
const total = 100 // JSONPlaceholder の合計
return NextResponse.json({
data: posts,
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }
})
}
// ======== POST /api/posts ========
export async function POST(request: NextRequest) {
const body = await request.json()
const validated = createPostSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: '検証に失敗しました', fields: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const post = await fetch(`${API}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validated.data),
}).then(r => r.json())
revalidateTag('posts')
return NextResponse.json(post, { status: 201 })
}
// app/api/posts/[id]/route.ts — 単一記事 CRUD
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const updatePostSchema = z.object({
title: z.string().min(2).max(200).optional(),
body: z.string().min(10).max(5000).optional(),
})
const API = 'https://jsonplaceholder.typicode.com'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await fetch(`${API}/posts/${params.id}`).then(r => {
if (!r.ok) throw new Error('見つかりません')
return r.json()
}).catch(() => null)
if (!post) {
return NextResponse.json({ error: '投稿が見つかりません' }, { status: 404 })
}
return NextResponse.json(post)
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const validated = updatePostSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: '検証に失敗しました', fields: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const updated = await fetch(`${API}/posts/${params.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validated.data),
}).then(r => r.json())
return NextResponse.json(updated)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await fetch(`${API}/posts/${params.id}`, { method: 'DELETE' })
return NextResponse.json({ success: true, id: params.id })
}
// middleware.ts — グローバル API ミドルウェア
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
export function middleware(request: NextRequest) {
const { pathname, origin } = request.nextUrl
if (!pathname.startsWith('/api/')) {
return NextResponse.next()
}
// 1. レート制限
const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
if (!rateLimit(ip, 30, 60_000)) {
return NextResponse.json({ error: 'リクエストが多すぎます' }, {
status: 429,
headers: { 'Retry-After': '60' }
})
}
// 2. ログ記録
console.log(`[API] ${request.method} ${pathname}`)
// 3. CORS
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key')
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: response.headers })
}
return response
}
export const config = {
matcher: '/api/:path*',
}
❓ よくある質問
route.ts で定義される標準的な HTTP エンドポイントで、サードパーティ API 統合、Webhook、モバイル呼び出しに適しています。サーバーアクションは 'use server' で定義される RPC スタイルのサーバーサイド関数で、ファーストパーティのフォーム操作に適しています。主な違い: ルートハンドラは HTTP 呼び出しが必要ですが、サーバーアクションは関数呼び出しです。ルートハンドラには CSRF 保護が含まれませんが、サーバーアクションには組み込まれています。NextResponse.json と単純な new Response() の違いは何ですか?NextResponse.json は NextResponse の便利なメソッドで、Content-Type: application/json を自動設定し、より優れた TypeScript 型推論を提供します。new Response(JSON.stringify(data), { headers: {'Content-Type': 'application/json'} }) と同等です。コードの簡潔さのために NextResponse.json の使用を推奨します。middleware.ts はデータベースを読み取れますか?export const runtime = 'edge' で Edge Runtime に切り替えられます。Edge Runtime はより低いレイテンシを提供しますが、API サポートが制限されています (fs や crypto などのネイティブ Node.js モジュールをサポートしません)。Edge Runtime は単純なプロキシ、認証検証、A/B テストに適しています。request.headers を使用して取得します: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() または request.headers.get('x-real-ip')。Vercel はデプロイ時にこれらのヘッダーを自動的に追加します。ローカル開発時は ::1 または 127.0.0.1 が返される場合があります。📖 まとめ
route.tsファイルは GET、POST、PUT、DELETE などの関数を使用して RESTful エンドポイントを定義します- Zod はリクエストボディとクエリパラメータを検証し、400 ステータスコードと詳細なエラー情報を返します
NextResponseは json()、redirect()、next()、rewrite() の4つのレスポンス形式をサポートします- middleware.ts はリクエストがページや API に到達する前に認証、ログ記録、CORS を統一的に処理します
matcher設定はミドルウェアのパスマッチング範囲を制御します- ミドルウェアまたはルートハンドラでレスポンスヘッダーを介して CORS を設定します
- レート制限: メモリ内マップまたは Redis で実装 (本番環境では Upstash Ratelimit を推奨)
📝 練習問題
-
基本問題 (⭐):
app/api/health/route.tsを作成し、JSON ヘルスチェック情報 (ステータス、タイムスタンプ、稼働時間) を返します。各 API 呼び出しとそのレスポンス時間をログに記録するミドルウェアを追加します。 -
応用問題 (⭐⭐): 完全な CRUD API を構築します:
app/api/books/route.ts(GET 一覧 + POST 作成) +app/api/books/[id]/route.ts(GET 詳細表示 + PUT 更新 + DELETE 削除)。Zod を使用してリクエストボディを検証します。ミドルウェアに API キー認証を追加します。 -
発展問題 (⭐⭐⭐): 「短縮 URL サービス」API を実装します:
app/api/shorten/route.ts(POST で URL を受け取り短縮 URL を返す)、app/api/[code]/route.ts(GET で短縮 URL を取得し 302 リダイレクトを実行)。レート制限 (IP ごとに1分あたり10件の短縮リンク)、CORS サポート、ミドルウェアアクセスログを追加します。短縮コードは JSON ファイルまたはメモリ内マップに保存します。