如何将 Next.js 用作前端的后端
Next.js 支持"前端后端"模式。这让你可以创建公共端点来处理 HTTP 请求并返回任何内容类型——不仅仅是 HTML。你还可以访问数据源并执行副作用,如更新远程数据。
如果你正在开始一个新项目,使用带有 --api 标志的 create-next-app 会自动在你的新项目的 app/ 文件夹中包含 一个示例 route.ts,演示如何创建 API 端点。
npx create-next-app@latest --api
注意:Next.js 后端功能不是完整的后端替代品。它们作为 API 层,具有以下特点:
- 可以公开访问
 - 处理任何 HTTP 请求
 - 可以返回任何内容类型
 
要实现此模式,请使用:
- 路由处理器
 middleware- 在 Pages 路由中,API 路由
 
公共端点
路由处理器是公共 HTTP 端点。任何客户端都可以访问它们。
使用 route.ts 或 route.js 文件约定创建路由处理器:
- TypeScript
 - JavaScript
 
export function GET(request: Request) {}
export function GET(request) {}
这处理发送到 /api 的 GET 请求。
对可能抛出异常的操作使用 try/catch 块:
- TypeScript
 - JavaScript
 
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 : '意外错误'
    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 : '意外错误'
    return new Response(message, { status: 500 })
  }
}
避免在发送给客户端的错误消息中暴露敏感信息。
要限制访问,请实现身份验证和授权。请参阅身份验证。
内容类型
路由处理器允许你提供非 UI 响应,包括 JSON、XML、图像、文件和纯文本。
Next.js 对常见端点使用文件约定:
你还可以定义自定义的端点,例如:
llms.txtrss.xml.well-known
例如,app/rss.xml/route.ts 为 rss.xml 创建路由处理器。
- TypeScript
 - JavaScript
 
export async function GET(request: Request) {
  const rssResponse = await fetch(/* rss 端点 */)
  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 端点 */)
  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 请求不携带请求体。
- TypeScript
 - JavaScript
 
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 })
}
注意:在将数据传递给其他系统之前验证数据
- TypeScript
 - JavaScript
 
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 : '意外异常'
    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 : '意外异常'
    return new Response(message, { status: 500 })
  }
}
你只能读取请求体一次。如果你需要再次读取它,请克隆请求:
- TypeScript
 - JavaScript
 
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 })
  }
}
export async function POST(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 })
  }
}
操作数据
路由处理器可以转换、过滤和聚合来自一个或多个源的数据。这可以将逻辑从前端中移出,避免暴露内部系统。
你还可以将重计算卸载到服务器,减少客户端电池和数据使用。
- TypeScript
 - JavaScript
 
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 : '意外异常'
    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 : '意外异常'
    return new Response(message, { status: 500 })
  }
}
注意:此示例使用
POST来避免将地理位置数据放在 URL 中。GET请求可能会被缓存或记录,这可能会暴露敏感信息。
代理到后端
你可以使用路由处理器作为到另一个后端的代理。在转发请求之前添加验证逻辑。
- TypeScript
 - JavaScript
 
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 : '意外异常'
    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 : '意外异常'
    return new Response(message, { status: 500 })
  }
}
或者使用:
NextRequest 和 NextResponse
Next.js 扩展了 Request 和 Response Web API,提供了简化常见操作的方法。这些扩展在路由处理器和中间件中都可用。
两者都提供了读取和操作 cookie 的方法。
NextRequest 包含 nextUrl 属性,它暴露来自传入请求的解析值,例如,它使访问请求路径名和搜索参数变得更容易。
NextResponse 提供了 next()、json()、redirect() 和 rewrite() 等辅助方法。
你可以将 NextRequest 传递给任何期望 Request 的函数。同样,你可以在期望 Response 的地方返回 NextResponse。
- TypeScript
 - JavaScript
 
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 })
}