Menu

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

Next.jsは「フロントエンド用バックエンド」パターンをサポートしています。これにより、HTTPリクエストを処理し、HTMLだけでなくあらゆるコンテンツタイプを返すパブリックエンドポイントを作成できます。データソースにアクセスし、リモートデータの更新などの副作用を実行することもできます。

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

Terminal
npx create-next-app@latest --api

補足:Next.jsのバックエンド機能は、完全なバックエンド置き換えではありません。以下のようなAPIレイヤーとして機能します:

  • パブリックに到達可能
  • あらゆるHTTPリクエストを処理
  • あらゆるコンテンツタイプを返すことが可能

このパターンを実装するには、以下を使用します:

パブリックエンドポイント

Route HandlersはパブリックなHTTPエンドポイントです。任意のクライアントがアクセスできます。

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

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

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

例外をスローする可能性のある操作にはtry/catchブロックを使用します:

/app/api/route.ts
TypeScript
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
TypeScript
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 })
}

マークアップを生成するために使用される入力をサニタイズしてください。

リクエストペイロードの処理

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

GETおよびHEADリクエストはボディを含みません。

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

補足:他のシステムに渡す前にデータを検証します

/app/api/send-email/route.ts
TypeScript
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 })
  }
}

リクエストボディは1回だけ読み取ることができます。再度読み取る必要がある場合はリクエストを複製します:

/app/api/clone/route.ts
TypeScript
export async function POST(request: Request) {
  try {
    const clonedRequest = request.clone()
 
    await request.body()
    await clonedRequest.body()
    await request.body() // エラーをスロー
 
    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) {
      /* エラーハンドリング */
    }
 
    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 })
  }
}
import { parseWeatherData } from '@/lib/weather'
 
export async function POST(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) {
      /* エラーハンドリング */
    }
 
    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
TypeScript
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.org')
  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 HandlersおよびProxyの両方で利用可能です。

両者ともクッキーを読み取り、操作するメソッドを提供します。

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

NextResponsenext()json()redirect()rewrite()などのヘルパーを提供します。

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

/app/echo-pathname/route.ts
TypeScript
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 })
}

NextRequestNextResponseについて詳しく学習します。

WebhookとコールバックURL

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

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

/app/webhook/route.ts
TypeScript
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
TypeScript
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, // セッションクッキー
  })
 
  return response
}

リダイレクト

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

redirectpermanentRedirectのリダイレクトについて詳しく学習します。

Proxy

プロジェクトごとに1つのproxyファイルのみが許可されます。config.matcherを使用して特定のパスをターゲットにします。proxyについて詳しく学習します。

proxyを使用して、リクエストがルートパスに到達する前に応答を生成します。

proxy.ts
TypeScript
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
TypeScript
import { NextResponse } from 'next/server'
 
export function proxy(request: Request) {
  if (request.nextUrl.pathname === '/proxy-this-path') {
    const rewriteUrl = new URL('https://nextjs.org')
    return NextResponse.rewrite(rewriteUrl)
  }
}

proxyが生成できる別のタイプの応答はリダイレクトです:

proxy.ts
TypeScript
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ヘッダーの詳細を学習します。

レート制限

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

/app/resource/route.ts
TypeScript
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をデータベースに保存して、リクエストサイズを削減します。

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

アクセス権を付与する前に、常に認証情報を検証します。認証と認可にproxy だけに頼らないようにしてください。

応答とバックエンドログから機密情報または不要なデータを削除します。

認証情報とAPIキーを定期的にローテーションします。

プリフライトリクエスト

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

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

ライブラリのパターン

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

/app/api/[...path]/route.ts
import { createHandler } from 'third-party-library'
 
const handler = createHandler({
  /* ライブラリ固有のオプション */
})
 
export const GET = handler
// または
export { handler as POST }

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

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

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

補足:サードパーティライブラリは、proxyをまだmiddlewareとして参照する場合があります。

その他の例

Router Handlersおよびproxy APIリファレンスの使用例をさらに参照してください。

これらの例には、クッキーヘッダーストリーミング、Proxy否定マッチング、およびその他の有用なコードスニペットの操作が含まれています。

注意事項

Server Components

Server Componentsのデータを直接ソースから取得し、Route Handlersを経由しないようにしてください。

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

オンデマンドでレンダリングされるServer Componentsの場合、Route Handlersからのフェッチは、ハンドラーとレンダープロセス間の追加HTTPラウンドトリップが原因で遅くなります。

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

Server Componentsはほとんどのデータフェッチニーズをカバーします。ただし、クライアント側からデータをフェッチする必要がある場合もあります:

  • クライアントのみのWeb APIに依存するデータ:
    • ジオロケーションAPI
    • Storage API
    • Audio API
    • File API
  • 頻繁にポーリングされるデータ

これらの場合は、swrreact-queryなどのコミュニティライブラリを使用してください。

Server Actions

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

Server Actionsはキューイングされます。データフェッチに使用すると、順序付きの実行が行われます。

exportモード

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

exportモードでは、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は、接続がタイムアウトで閉じられるか、応答が生成された後に閉じられるため、機能しません。