コンテンツにスキップ
App RouterガイドBackend for Frontend

Next.js をフロントエンドのバックエンドとして使用する方法

Next.js は「Backend for Frontend」パターンをサポートしています。これにより、HTTP リクエストを処理して HTML 以外の任意のコンテンツタイプを返す公開エンドポイントを作成できます。データソースにアクセスしたり、リモートデータの更新などの副作用を実行することもできます。

新規プロジェクトを開始する場合、--api フラグを付けて create-next-app を使用すると、新しいプロジェクトの app/ フォルダーに例の route.ts が自動的に含まれ、API エンドポイントの作成方法が示されます。

ターミナル
npx create-next-app@latest --api

知っておくと良いこと:Next.js のバックエンド機能は、完全なバックエンドの代替ではありません。API レイヤーとして機能し、

  • 公開からアクセス可能
  • 任意の HTTP リクエストを処理
  • 任意のコンテンツタイプを返すことができる

このパターンを実装するには、

公開エンドポイント

Route Handlers は公開 HTTP エンドポイントです。あらゆるクライアントがアクセスできます。

route.ts または route.js ファイル規約を使用して Route Handler を作成します。

/app/api/route.ts
export function GET(request: Request) {}

これは /api に送信された GET リクエストを処理します。

例外が発生する可能性のある操作には try/catch ブロックを使用します。

/app/api/route.ts
import { submit } from '@/lib/submit'
 
export async function POST(request: Request) {
  try {
    await submit(request)
    return new Response(null, { status: 204 })
  } catch (reason) {
    const message =
      reason instanceof Error ? reason.message : 'Unexpected error'
 
    return new Response(message, { status: 500 })
  }
}

クライアントに送信されるエラーメッセージに機密情報を公開しないでください。

アクセスを制限するには、認証と認可を実装してください。 認証を参照してください。

コンテンツタイプ

Route Handlers は、JSON、XML、画像、ファイル、プレーンテキストなど、UI 以外のレスポンスを返すことができます。

Next.js は一般的なエンドポイントにファイル規約を使用します。

カスタムファイルも定義できます。例えば、

  • llms.txt
  • rss.xml
  • .well-known

例えば、app/rss.xml/route.tsrss.xml のための Route Handler を作成します。

/app/rss.xml/route.ts
export async function GET(request: Request) {
  const rssResponse = await fetch(/* rss endpoint */)
  const rssData = await rssResponse.json()
 
  const rssFeed = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
 <title>${rssData.title}</title>
 <description>${rssData.description}</description>
 <link>${rssData.link}</link>
 <copyright>${rssData.copyright}</copyright>
 ${rssData.items.map((item) => {
   return `<item>
    <title>${item.title}</title>
    <description>${item.description}</description>
    <link>${item.link}</link>
    <pubDate>${item.publishDate}</pubDate>
    <guid isPermaLink="false">${item.guid}</guid>
 </item>`
 })}
</channel>
</rss>`
 
  const headers = new Headers({ 'content-type': 'application/xml' })
 
  return new Response(rssFeed, { headers })
}

マークアップの生成に使用する入力はすべてサニタイズしてください。

リクエストペイロードの消費

.json().formData().text() のような Request インスタンスメソッド を使用してリクエストボディにアクセスします。

GET および HEAD リクエストにはボディがありません。

/app/api/echo-body/route.ts
export async function POST(request: Request) {
  const res = await request.json()
  return Response.json({ res })
}

知っておくと良いこと:他のシステムに渡す前にデータを検証してください。

/app/api/send-email/route.ts
import { sendMail, validateInputs } from '@/lib/email-transporter'
 
export async function POST(request: Request) {
  const formData = await request.formData()
  const email = formData.get('email')
  const contents = formData.get('contents')
 
  try {
    await validateInputs({ email, contents })
    const info = await sendMail({ email, contents })
 
    return Response.json({ messageId: info.messageId })
  } catch (reason) {
    const message =
      reason instanceof Error ? reason.message : 'Unexpected exception'
 
    return new Response(message, { status: 500 })
  }
}

リクエストボディは一度しか読み取れません。再度読み取る必要がある場合は、リクエストをクローンしてください。

/app/api/clone/route.ts
export async function POST(request: Request) {
  try {
    const clonedRequest = request.clone()
 
    await request.body()
    await clonedRequest.body()
    await request.body() // Throws error
 
    return new Response(null, { status: 204 })
  } catch {
    return new Response(null, { status: 500 })
  }
}

データの操作

Route Handlers は、1 つ以上のソースからのデータを変換、フィルタリング、集計できます。これにより、ロジックをフロントエンドから分離し、内部システムを公開するのを防ぎます。

サーバーに重い計算をオフロードし、クライアントのバッテリーとデータ使用量を削減することもできます。

import { parseWeatherData } from '@/lib/weather'
 
export async function POST(request: Request) {
  const body = await request.json()
  const searchParams = new URLSearchParams({ lat: body.lat, lng: body.lng })
 
  try {
    const weatherResponse = await fetch(`${weatherEndpoint}?${searchParams}`)
 
    if (!weatherResponse.ok) {
      /* handle error */
    }
 
    const weatherData = await weatherResponse.text()
    const payload = parseWeatherData.asJSON(weatherData)
 
    return new Response(payload, { status: 200 })
  } catch (reason) {
    const message =
      reason instanceof Error ? reason.message : 'Unexpected exception'
 
    return new Response(message, { status: 500 })
  }
}

知っておくと良いこと:この例では、ジオロケーションデータを URL に含めないように POST を使用しています。GET リクエストはキャッシュされたりログに記録されたりする可能性があり、機密情報が公開される可能性があります。

バックエンドへのプロキシ

Route Handler を別のバックエンドの proxy として使用できます。リクエストを転送する前に検証ロジックを追加します。

/app/api/[...slug]/route.ts
import { isValidRequest } from '@/lib/utils'
 
export async function POST(request: Request, { params }) {
  const clonedRequest = request.clone()
  const isValid = await isValidRequest(clonedRequest)
 
  if (!isValid) {
    return new Response(null, { status: 400, statusText: 'Bad Request' })
  }
 
  const { slug } = await params
  const pathname = slug.join('/')
  const proxyURL = new URL(pathname, 'https://nextjs.dokyumento.jp')
  const proxyRequest = new Request(proxyURL, request)
 
  try {
    return fetch(proxyRequest)
  } catch (reason) {
    const message =
      reason instanceof Error ? reason.message : 'Unexpected exception'
 
    return new Response(message, { status: 500 })
  }
}

または使用する

NextRequest と NextResponse

Next.js は、一般的な操作を簡略化するメソッドを備えた Request および Response Web API を拡張しています。これらの拡張機能は、Route Handler と Proxy の両方で利用できます。

どちらも Cookie の読み書きのためのメソッドを提供します。

NextRequest には nextUrl プロパティが含まれており、受信したリクエストの解析済みの値を公開します。例えば、リクエストのパス名や検索パラメータへのアクセスが容易になります。

NextResponse は、next()json()redirect()rewrite() などのヘルパーを提供します。

Request を期待する任意の関数に NextRequest を渡すことができます。同様に、Response が期待される場所で NextResponse を返すことができます。

/app/echo-pathname/route.ts
import { type NextRequest, NextResponse } from 'next/server'
 
export async function GET(request: NextRequest) {
  const nextUrl = request.nextUrl
 
  if (nextUrl.searchParams.get('redirect')) {
    return NextResponse.redirect(new URL('/', request.url))
  }
 
  if (nextUrl.searchParams.get('rewrite')) {
    return NextResponse.rewrite(new URL('/', request.url))
  }
 
  return NextResponse.json({ pathname: nextUrl.pathname })
}

NextRequest および NextResponse についてさらに詳しく学びましょう。

Webhook とコールバックURL

Route Handlers を使用して、サードパーティアプリケーションからのイベント通知を受信します。

例えば、CMS でコンテンツが変更されたときにルートを再検証します。変更時に特定の endpoint を呼び出すように CMS を設定します。

/app/webhook/route.ts
import { type NextRequest, NextResponse } from 'next/server'
 
export async function GET(request: NextRequest) {
  const token = request.nextUrl.searchParams.get('token')
 
  if (token !== process.env.REVALIDATE_SECRET_TOKEN) {
    return NextResponse.json({ success: false }, { status: 401 })
  }
 
  const tag = request.nextUrl.searchParams.get('tag')
 
  if (!tag) {
    return NextResponse.json({ success: false }, { status: 400 })
  }
 
  revalidateTag(tag)
 
  return NextResponse.json({ success: true })
}

コールバックURLも別のユースケースです。ユーザーがサードパーティのフローを完了すると、サードパーティはそのユーザーをコールバックURLに送信します。Route Handler を使用してレスポンスを検証し、ユーザーをリダイレクトする場所を決定します。

/app/auth/callback/route.ts
import { type NextRequest, NextResponse } from 'next/server'
 
export async function GET(request: NextRequest) {
  const token = request.nextUrl.searchParams.get('session_token')
  const redirectUrl = request.nextUrl.searchParams.get('redirect_url')
 
  const response = NextResponse.redirect(new URL(redirectUrl, request.url))
 
  response.cookies.set({
    value: token,
    name: '_token',
    path: '/',
    secure: true,
    httpOnly: true,
    expires: undefined, // session cookie
  })
 
  return response
}

リダイレクト

app/api/route.ts
import { redirect } from 'next/navigation'
 
export async function GET(request: Request) {
  redirect('https://nextjs.dokyumento.jp/')
}

redirect および permanentRedirect のリダイレクトについてさらに学習する。

プロキシ

プロジェクトごとに 1 つの proxy ファイルのみが許可されます。特定のパスを対象にするには config.matcher を使用します。 proxy についてさらに学習する。

リクエストがルートパスに到達する前にレスポンスを生成するために proxy を使用します。

proxy.ts
import { isAuthenticated } from '@lib/auth'
 
export const config = {
  matcher: '/api/:function*',
}
 
export function proxy(request: Request) {
  if (!isAuthenticated(request)) {
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }
}

proxy を使用してリクエストをプロキシすることもできます。

proxy.ts
import { NextResponse } from 'next/server'
 
export function proxy(request: Request) {
  if (request.nextUrl.pathname === '/proxy-this-path') {
    const rewriteUrl = new URL('https://nextjs.dokyumento.jp')
    return NextResponse.rewrite(rewriteUrl)
  }
}

proxy が生成できるもう 1 つのレスポンスタイプはリダイレクトです。

proxy.ts
import { NextResponse } from 'next/server'
 
export function proxy(request: Request) {
  if (request.nextUrl.pathname === '/v1/docs') {
    request.nextUrl.pathname = '/v2/docs'
    return NextResponse.redirect(request.nextUrl)
  }
}

セキュリティ

ヘッダーの操作

ヘッダーの配置に注意し、受信したリクエストヘッダーを送信レスポンスに直接渡さないようにしてください。

  • アップストリームリクエストヘッダー:Proxy では、NextResponse.next({ request: { headers } }) はサーバーが受信するヘッダーを変更し、クライアントには公開しません。
  • レスポンスヘッダーnew Response(..., { headers })NextResponse.json(..., { headers })NextResponse.next({ headers })、または response.headers.set(...) はヘッダーをクライアントに返します。これらのヘッダーに機密値が追加された場合、クライアントに表示されます。

Proxy の NextResponse ヘッダーについては、NextResponse headers in Proxy で詳細を確認してください。

レート制限

Next.js バックエンドでレート制限を実装できます。コードベースのチェックに加えて、ホストが提供するレート制限機能を有効にしてください。

/app/resource/route.ts
import { NextResponse } from 'next/server'
import { checkRateLimit } from '@/lib/rate-limit'
 
export async function POST(request: Request) {
  const { rateLimited } = await checkRateLimit(request)
 
  if (rateLimited) {
    return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 })
  }
 
  return new Response(null, { status: 204 })
}

ペイロードの検証

受信したリクエストデータを決して信頼しないでください。コンテンツタイプとサイズを検証し、使用前に XSS に対してサニタイズしてください。

タイムアウトを使用して、不正利用を防ぎ、サーバーリソースを保護します。

ユーザー生成の静的アセットは専用のサービスに保存します。可能な場合は、ブラウザからアップロードし、データベースに返された URI を保存して、リクエストサイズを削減します。

保護されたリソースへのアクセス

アクセスを許可する前に、必ず資格情報を検証してください。認証と認可のためにプロキシだけに頼らないでください。

レスポンスとバックエンドログから、機密情報や不要なデータを削除してください。

資格情報と API キーを定期的にローテーションしてください。

プリフライトリクエスト

プリフライトリクエストは OPTIONS メソッドを使用して、オリジン、メソッド、ヘッダーに基づいてリクエストが許可されるかどうかをサーバーに問い合わせます。

OPTIONS が定義されていない場合、Next.js はそれを自動的に追加し、他の定義されたメソッドに基づいて Allow ヘッダーを設定します。

ライブラリパターン

コミュニティライブラリは、Route Handlers にファクトリパターンをよく使用します。

/app/api/[...path]/route.ts
import { createHandler } from 'third-party-library'
 
const handler = createHandler({
  /* library-specific options */
})
 
export const GET = handler
// or
export { handler as POST }

これは GET および POST リクエストの共有ハンドラーを作成します。ライブラリは、リクエストの methodpathname に基づいて動作をカスタマイズします。

ライブラリは proxy ファクトリを提供することもできます。

proxy.ts
import { createMiddleware } from 'third-party-library'
 
export default createMiddleware()

知っておくと良いこと:サードパーティライブラリは、proxymiddleware と参照することがあります。

その他の例

Route Handler および proxy API リファレンスで使用されるその他の例を参照してください。

これらの例には、Cookieヘッダーストリーミング、Proxy の ネガティブマッチング、およびその他の便利なコードスニペットが含まれます。

注意点

サーバーコンポーネント

Server Components では、Route Handler を介さずに、ソースから直接データを取得します。

ビルド時に事前レンダリングされる Server Components では、Route Handler を使用するとビルドステップが失敗します。これは、ビルド中はこれらのリクエストをリッスンするサーバーが存在しないためです。

オンデマンドでレンダリングされる Server Components では、ハンドラーとレンダリングプロセスの間の追加の HTTP ラウンドトリップにより、Route Handler からの取得は遅くなります。

サーバーサイドの fetch リクエストは絶対 URL を使用します。これは、外部サーバーへの HTTP ラウンドトリップを意味します。開発中は、自身の開発サーバーが外部サーバーとして機能します。ビルド時にはサーバーはなく、実行時には、サーバーは公開 facing ドメイン経由で利用可能です。

Server Components はほとんどのデータ取得ニーズをカバーしています。ただし、クライアントサイドでのデータ取得が必要になる場合があります。

  • クライアント専用 Web API に依存するデータ
    • ジオロケーション API
    • ストレージ API
    • オーディオ API
    • ファイル API
  • 頻繁にポーリングされるデータ

これらの場合は、swrreact-query のようなコミュニティライブラリを使用します。

サーバーアクション

Server Actions を使用すると、クライアントからサーバーサイドコードを実行できます。それらの主な目的は、フロントエンドクライアントからデータを変更することです。

Server Actions はキューに入れられます。データ取得のためにそれらを使用すると、順次実行が発生します。

export モード

export モードは、ランタイムサーバーなしで静的サイトを出力します。Next.js ランタイムを必要とする機能は、このモードが静的サイトを生成し、ランタイムサーバーがないため、サポートされていません

export mode では、GET Route Handlers のみがサポートされます。これは、dynamic ルートセグメント設定と組み合わせて、'force-static' に設定されます。

これは、静的な HTML、JSON、TXT などのファイルを生成するために使用できます。

app/hello-world/route.ts
export const dynamic = 'force-static'
 
export function GET() {
  return new Response('Hello World', { status: 200 })
}

デプロイメント環境

一部のホストは Route Handlers を lambda 関数としてデプロイします。これは意味します

  • Route Handlers はリクエスト間でデータを共有できません。
  • 環境がファイルシステムへの書き込みをサポートしない場合があります。
  • 長時間のハンドラーはタイムアウトにより終了される可能性があります。
  • WebSocket は、接続がタイムアウトで閉じられるか、レスポンスが生成された後に機能しません。

APIリファレンス

Route Handlers と Proxy についてさらに学習する。