跳到内容
App Router指南前端的后端

如何将 Next.js 用作前端的后端

Next.js 支持“前端的后端”模式。这允许您创建公共端点来处理 HTTP 请求并返回任何内容类型(不仅限于 HTML)。您还可以访问数据源并执行更新远程数据等副作用。

如果您正在启动一个新项目,使用带有 --api 标志的 create-next-app 会自动在您新项目的 app/ 文件夹中包含一个示例 route.ts,演示如何创建 API 端点。

终端
npx create-next-app@latest --api

须知:Next.js 后端功能并非完整的后端替代品。它们充当了一个 API 层,它

  • 可公开访问
  • 处理任何 HTTP 请求
  • 可以返回任何内容类型

要实现此模式,请使用

公共端点

路由处理程序是公共 HTTP 端点。任何客户端都可以访问它们。

使用 route.tsroute.js 文件约定创建路由处理程序

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

这处理发送到 /apiGET 请求。

对于可能抛出异常的操作,请使用 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 })
  }
}

避免在发送给客户端的错误消息中暴露敏感信息。

要限制访问,请实施身份验证和授权。请参阅身份验证

内容类型

路由处理程序允许您提供非 UI 响应,包括 JSON、XML、图像、文件和纯文本。

Next.js 为常见端点使用文件约定

您还可以定义自定义的,例如

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

例如,app/rss.xml/route.tsrss.xml 创建一个路由处理程序。

/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 })
}

清理用于生成标记的任何输入。

消费请求负载

使用 Request 实例方法,如 .json().formData().text() 来访问请求体。

GETHEAD 请求不带请求体。

/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 })
  }
}

操作数据

路由处理程序可以转换、过滤和聚合来自一个或多个来源的数据。这使得逻辑脱离前端,并避免暴露内部系统。

您还可以将繁重的计算卸载到服务器,并减少客户端电池和数据使用。

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 })
  }
}

须知:此示例使用 POST 以避免将地理位置数据放入 URL。GET 请求可能会被缓存或记录,这可能会暴露敏感信息。

代理到后端

您可以将路由处理程序用作到另一个后端的 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.net.cn')
  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 扩展了 RequestResponse Web API,提供了简化常见操作的方法。这些扩展在路由处理程序和代理中都可用。

两者都提供了读取和操作 cookie 的方法。

NextRequest 包含 nextUrl 属性,该属性公开了来自传入请求的解析值,例如,它使得访问请求路径名和搜索参数更容易。

NextResponse 提供了 next()json()redirect()rewrite() 等助手。

您可以将 NextRequest 传递给任何期望 Request 的函数。同样,您可以在期望 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 })
}

了解更多关于 NextRequestNextResponse 的信息。

Webhooks 和回调 URL

使用路由处理程序接收来自第三方应用程序的事件通知。

例如,当 CMS 中的内容发生更改时重新验证路由。配置 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。使用路由处理程序验证响应并决定将用户重定向到何处。

/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.net.cn/')
}

redirectpermanentRedirect 中了解更多关于重定向的信息

代理

每个项目只允许一个 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.net.cn')
    return NextResponse.rewrite(rewriteUrl)
  }
}

proxy 可以产生的另一种响应类型是重定向

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)
  }
}

安全

使用头部

请注意头部信息去向,避免直接将传入请求头部传递给传出响应。

  • 上游请求头部:在代理中,NextResponse.next({ request: { headers } }) 修改您的服务器接收的头部,但不会将其暴露给客户端。
  • 响应头部new Response(..., { headers })NextResponse.json(..., { headers })NextResponse.next({ headers })response.headers.set(...) 将头部发送回客户端。如果将敏感值附加到这些头部,它们将对客户端可见。

代理中的 NextResponse 请求头中了解更多。

速率限制

您可以在 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 头部。

库模式

社区库通常使用工厂模式来处理路由处理程序。

/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 }

这为 GETPOST 请求创建了一个共享处理程序。该库根据请求中的 methodpathname 定制行为。

库还可以提供 proxy 工厂。

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

须知:第三方库可能仍将 proxy 称为 middleware

更多示例

请参阅有关使用 路由器处理程序proxy API 参考的更多示例。

这些示例包括处理 CookiesHeadersStreaming、Proxy 负匹配 和其他有用的代码片段。

注意事项

服务器组件

在服务器组件中直接从其源获取数据,而不是通过路由处理程序。

对于在构建时预渲染的服务器组件,使用路由处理程序将导致构建步骤失败。这是因为在构建期间没有服务器监听这些请求。

对于按需渲染的服务器组件,从路由处理程序获取数据会更慢,因为处理程序和渲染过程之间需要额外的 HTTP 往返。

服务器端 fetch 请求使用绝对 URL。这意味着需要 HTTP 往返到一个外部服务器。在开发过程中,您自己的开发服务器充当外部服务器。在构建时没有服务器,在运行时,服务器通过您的公共域名可用。

服务器组件涵盖了大多数数据获取需求。但是,在客户端获取数据可能是必要的,用于

  • 依赖于仅限客户端的 Web API 的数据
    • 地理位置 API
    • 存储 API
    • 音频 API
    • 文件 API
  • 频繁轮询的数据

对于这些,请使用社区库,例如 swrreact-query

服务器操作

服务器操作允许您从客户端运行服务器端代码。它们的主要目的是从前端客户端改变数据。

服务器操作是排队的。使用它们进行数据获取会引入顺序执行。

导出模式

导出模式会输出一个没有运行时服务器的静态站点。需要 Next.js 运行时功能 不支持,因为此模式会生成一个静态站点,并且没有运行时服务器。

导出模式 中,仅支持 GET 路由处理程序,并结合路由段配置 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 })
}

部署环境

有些主机将路由处理程序部署为 lambda 函数。这意味着

  • 路由处理程序无法在请求之间共享数据。
  • 环境可能不支持写入文件系统。
  • 长时间运行的处理程序可能会因为超时而被终止。
  • WebSockets 将无法工作,因为连接在超时后或生成响应后关闭。

API 参考

了解更多关于路由处理程序和代理的信息