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 请求
- 可以返回任何内容类型
要实现此模式,请使用:
proxy路由处理器是公共 HTTP 端点。任何客户端都可以访问它们。
使用 route.ts 或 route.js 文件约定创建路由处理器:
export function GET(request: Request) {}export function GET(request) {}这处理发送到 /api 的 GET 请求。
对可能抛出异常的操作使用 try/catch 块:
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 })
}
}import { submit } from '@/lib/submit'
export async function POST(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 })
}
}避免在发送给客户端的错误消息中暴露敏感信息。
若要限制访问,请实现身份验证和授权。请参阅 Authentication。
路由处理器允许您提供非 UI 响应,包括 JSON、XML、图片、文件和纯文本。
Next.js 为常用端点使用文件约定:
sitemap.xmlopengraph-image.jpg, twitter-imagemanifest.jsonrobots.txt您还可以定义自定义的,例如:
llms.txtrss.xml.well-known例如,app/rss.xml/route.ts 为 rss.xml 创建一个路由处理器。
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 })
}export async function GET(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 请求不携带请求体。
export async function POST(request: Request) {
const res = await request.json()
return Response.json({ res })
}export async function POST(request) {
const res = await request.json()
return Response.json({ res })
}须知:在将数据传递给其他系统之前进行验证。
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 })
}
}import { sendMail, validateInputs } from '@/lib/email-transporter'
export async function POST(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 })
}
}请求体只能读取一次。如果需要再次读取,请克隆该请求:
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 })
}
}export async function POST(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 })
}
}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) {
/* 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。在转发请求之前添加验证逻辑。
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 })
}
}import { isValidRequest } from '@/lib/utils'
export async function POST(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 })
}
}或者使用:
Next.js 扩展了 Request 和 Response Web API,增加了简化常见操作的方法。这些扩展在路由处理器和 Proxy 中都可用。
两者都提供了读取和操作 cookie 的方法。
NextRequest 包含 nextUrl 属性,该属性暴露了传入请求的解析值,例如,它使得访问请求路径名和搜索参数变得更容易。
NextResponse 提供了 next()、json()、redirect() 和 rewrite() 等辅助方法。
您可以将 NextRequest 传递给任何期望 Request 的函数。同样,您可以在期望 Response 的地方返回 NextResponse。
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 })
}import { NextResponse } from 'next/server'
export async function GET(request) {
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。
使用路由处理器接收来自第三方应用程序的事件通知。
例如,当 CMS 中的内容发生变化时重新验证路由。配置 CMS 以在内容更改时调用特定的端点。
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 })
}import { NextResponse } from 'next/server'
export async function GET(request) {
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。使用路由处理器验证响应并决定将用户重定向到何处。
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
}import { NextResponse } from 'next/server'
export async function GET(request) {
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
}import { redirect } from 'next/navigation'
export async function GET(request: Request) {
redirect('https://nextjs.org/')
}import { redirect } from 'next/navigation'
export async function GET(request) {
redirect('https://nextjs.org/')
}了解更多关于 redirect 和 permanentRedirect 中的重定向。
每个项目只允许一个 proxy 文件。使用 config.matcher 定位特定路径。了解更多关于 proxy。
使用 proxy 在请求到达路由路径之前生成响应。
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 }
)
}
}import { isAuthenticated } from '@lib/auth'
export const config = {
matcher: '/api/:function*',
}
export function proxy(request) {
if (!isAuthenticated(request)) {
return Response.json(
{ success: false, message: 'authentication failed' },
{ status: 401 }
)
}
}您也可以使用 proxy 代理请求:
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)
}
}import { NextResponse } from 'next/server'
export function proxy(request) {
if (request.nextUrl.pathname === '/proxy-this-path') {
const rewriteUrl = new URL('https://nextjs.org')
return NextResponse.rewrite(rewriteUrl)
}
}proxy 可以生成的另一种响应类型是重定向:
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)
}
}import { NextResponse } from 'next/server'
export function proxy(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 headers in Proxy 中了解更多信息。
您可以在 Next.js 后端实现速率限制。除了基于代码的检查外,还应启用您的主机提供的任何速率限制功能。
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 })
}import { NextResponse } from 'next/server'
import { checkRateLimit } from '@/lib/rate-limit'
export async function POST(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 标头。
社区库通常为路由处理器使用工厂模式。
import { createHandler } from 'third-party-library'
const handler = createHandler({
/* library-specific options */
})
export const GET = handler
// or
export { handler as POST }这为 GET 和 POST 请求创建了一个共享处理器。该库根据请求中的 method 和 pathname 定制行为。
库还可以提供一个 proxy 工厂。
import { createMiddleware } from 'third-party-library'
export default createMiddleware()须知:第三方库可能仍然将
proxy称为middleware。
请参阅更多关于使用 Router Handlers 和 proxy API 参考的示例。
这些示例包括使用 Cookies、Headers、Streaming、Proxy negative matching 以及其他有用的代码片段。
在 Server Components 中直接从其源获取数据,而不是通过路由处理器。
对于在构建时预渲染的 Server Components,使用路由处理器将导致构建失败。这是因为在构建过程中,没有服务器监听这些请求。
对于按需渲染的 Server Components,从路由处理器获取数据会更慢,因为处理程序和渲染过程之间需要额外的 HTTP 往返。
服务端
fetch请求使用绝对 URL。这意味着需要一次 HTTP 往返到外部服务器。在开发过程中,您自己的开发服务器充当外部服务器。在构建时没有服务器,而在运行时,服务器通过您的公共域名可用。
Server Components 覆盖了大多数数据获取需求。但是,在客户端获取数据可能在以下情况下是必要的:
对于这些情况,请使用 swr 或 react-query 等社区库。
Server Actions 允许您从客户端运行服务器端代码。它们的主要目的是从前端客户端修改数据。
Server Actions 是排队的。使用它们进行数据获取会引入顺序执行。
export 模式export 模式输出一个不带运行时服务器的静态站点。需要 Next.js 运行时才能使用的功能不受支持,因为此模式生成的是静态站点,并且没有运行时服务器。
在 export 模式下,仅支持 GET 路由处理器,并结合将 dynamic 路由段配置设置为 'force-static'。
这可用于生成静态 HTML、JSON、TXT 或其他文件。
export const dynamic = 'force-static'
export function GET() {
return new Response('Hello World', { status: 200 })
}一些主机将路由处理器部署为 Lambda 函数。这意味着: