首页 / Nuxt 4 入门教程 / 服务端路由 server routes

Nuxt 4 入门教程

服务端路由 server routes

本教程共 50 篇 · 第 32 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

NuxtNuxt4serverAPIeventHandlerH3Nitro

本节目标:能在 server/ 目录下写出自己的 API 接口,读取地址参数、查询参数和请求体,并返回 JSON 或自定义状态码。

前面我们一直在写「浏览器里跑」的页面。但 Nuxt 是全栈框架——同一套代码里,你也能写跑在服务器上的接口(API)。比如要查数据库、调第三方接口、生成站点地图,都可以在服务端做,再把结果给前端。Nuxt 把这部分能力放在项目根目录的 server/ 文件夹里,自动扫描、热更新、自动导入,体验和写页面差不多。

32-1

Nuxt 的服务器不是自己从零造的,而是基于一个叫 Nitro 的引擎(下一章细讲)。Nitro 内部用了一个轻量的 HTTP 框架 h3,我们写的每一个接口,本质上就是一个 h3 的「事件处理器」(event handler)。

核心写法只有一句:导出一个用 defineEventHandler 包起来的函数。每个文件就是一个接口。Nuxt 会自动根据文件路径生成 URL。

32-2

server/api/ 下新建一个文件,导出一个默认处理函数:

export default defineEventHandler((event) => {
  return {
    hello: 'world',
  }
})

就这么几行,/api/hello 这个接口就活了。注意目录名 api,它会自动给路由加上 /api 前缀。函数可以直接 return 一个对象,Nuxt 会用 h3 帮你序列化成 JSON 并返回 200。

在页面里调用它,和调普通接口一样,但 Nuxt 有个贴心优化:用 $fetchuseFetch 时,如果是服务端发起请求,会直接调用函数而不走网络,省一次往返。

<script setup lang="ts">
const { data } = await useFetch('/api/hello')
</script>

<template>
  <pre>{{ data }}</pre>
</template>
Tip

defineEventHandlereventHandler 是同一个东西的别名,写哪个都行。函数可以是 async,返回 Promise 会被自动等待。

写服务端接口时有两个体验上的小惊喜:一是热更新,你改了 server/api/ 下的文件,不用重启服务,刷新页面就能看到新逻辑;二是自动导入,像 getRouterParamreadBodycreateError 这些 h3 助手函数不用你手动 import,直接用就行,和写页面组件时的自动导入一样顺手。这让「前后端同项目」的开发节奏非常统一,不用在两边切换不同的心智模型。

32-3

接口常常要带参数,比如 /api/user/123 里的 123。文件名叫 [参数名].ts,参数就从 event.context.params 拿到。更方便的是 h3 提供的 getRouterParam

export default defineEventHandler((event) => {
  const id = getRouterParam(event, 'id')

  return { userId: id }
})

访问 /api/user/123,返回 {"userId":"123"}。想要带类型校验,可以用 getValidatedRouterParams 配合 Zod 之类的方案,运行期既校验又出类型。

32-4

URL 里 ? 后面的部分叫查询参数,比如 /api/search?q=nuxt&page=2。用 getQuery 读取:

export default defineEventHandler((event) => {
  const query = getQuery(event)

  return { keyword: query.q, page: query.page }
})
Note

这里文件名多了 .get,表示只接受 GET 请求。下面马上讲这种「按方法拆文件」的写法。

32-5

前端用 POST 提交表单、JSON 数据时,内容在请求体里。用 readBody 读:

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  return { received: body }
})

前端这样发:

<script setup lang="ts">
async function submit() {
  const { body } = await $fetch('/api/submit', {
    method: 'post',
    body: { test: 123 },
  })
}
</script>
Warning

readBody 只在有请求体时能用。如果在 GET 请求里调 readBody,h3 会抛 405 Method Not Allowed。所以处理 body 的文件名通常带 .post

32-6

同一个路径,GET 和 POST 想做不同事?把方法名加在文件名后面即可:.get.post.put.delete……

export default defineEventHandler(() => '这是 GET 的处理')
export default defineEventHandler(() => '这是 POST 的处理')

访问 /api/test:GET 返回第一段,POST 返回第二段,其他方法返回 405。你也能在目录里用 index.get.tsindex.post.ts 来组织一组接口,形成 API 命名空间。

32-7

server/api/ 的接口都带 /api 前缀。如果你想做一个不带前缀的路由(比如 /health),就放进 server/routes/

export default defineEventHandler(() => 'OK')

访问 http://localhost:3000/health 即可。注意服务端路由目前不像页面路由那样支持完整的动态嵌套,能力比 pages/ 弱一些。

偶尔你会想要一个「兜底」路由,接住所有没匹配到的请求。在 server/api/server/routes/ 里用 [...] 命名文件即可,比如 server/api/foo/[...].ts 会接管所有 /api/foo/... 下面没被具体路由认领的路径。你还能给 catch-all 起名字(如 [...slug].ts),之后从 event.context.params.slug 拿到那段路径。它适合做通用的 404 处理或把未知请求转给前端路由。

32-8

默认成功是 200。想返回别的状态码,用 setResponseStatus

export default defineEventHandler((event) => {
  setResponseStatus(event, 202)
  return { message: '已接受' }
})

遇到错误,直接 throw 一个 createError,Nuxt 会转成对应的 HTTP 错误;没有捕获的异常则统一返回 500:

export default defineEventHandler((event) => {
  const id = getRouterParam(event, 'id')

  if (!id || Number.isNaN(Number(id))) {
    throw createError({
      statusCode: 400,
      statusMessage: 'ID 必须是数字',
    })
  }

  return { ok: true }
})

32-9

接口里若有一些公共逻辑(比如格式化、鉴权包装),可以抽到 server/utils/ 目录,自动导入,随处可用:

export function formatUser(raw: { name: string }) {
  return { displayName: raw.name.toUpperCase() }
}
export default defineEventHandler(() => {
  return formatUser({ name: 'nuxt' })
})

当路由嵌套很深、相对路径写起来很烦时,Nuxt 4.3+ 提供了 #server 别名,从 server/ 内任意位置统一导入:

// 不用 '../../../utils/formatUser' 这种相对路径
import { formatUser } from '#server/utils/formatUser'
Important

服务端代码(server/)和前端 Vue 代码(app/)运行在不同环境,不能互相 import。别在 server 路由里引入组件或 composables,也别在前端引入 server 专用代码。

32-10

一个 event handler 不只是「return 个对象」就完事。h3 在 event 参数上挂了一整套助手,常见几类:

  • 读请求:getRouterParamgetQueryreadBody(前面讲过),还有 getHeader 读请求头、getCookie 读 cookie。
  • 写响应:setResponseStatus 改状态码、setResponseHeader 加响应头、setCookie 写 cookie、appendResponseHeader 追加头。
  • 工具:getRequestURL 拿完整 URL、getRequestHost 拿主机名、sendRedirect 做跳转、createError 抛错。
  • 解析:readMultipartFormData 读文件上传、getRequestWebStream 读请求流。

所以函数体里你可以自由组合这些助手:比如读 cookie、校验后写响应头,再返回数据。只要最后 return 的是合法响应(对象、字符串、Buffer 等)即可。

32-11

除了 JSON,接口还能吐别的东西。

返回文件并触发下载,用响应头设好类型和附件名,再 return 文件内容(Buffer 或字符串):

export default defineEventHandler((event) => {
  setResponseHeader(event, 'Content-Type', 'application/pdf')
  setResponseHeader(event, 'Content-Disposition', 'attachment; filename="report.pdf"')
  return Buffer.from('%PDF-1.4 ...') // 实际项目里换成真实文件内容
})

直接 return 一个 Buffer 时,h3 会按内容类型发送,省去手动调底层发送函数。

流式响应适合大模型回答、大文件下载这类「边生成边发」的场景。用 event.node.res 拿底层响应对象直接写:

export default defineEventHandler((event) => {
  const res = event.node.res
  res.setHeader('Content-Type', 'text/plain; charset=utf-8')
  res.write('第一行\n')
  // 真实项目里这里可以分块、按节奏推送
  res.end('结束')
})

这种写法绕开了 h3 的自动序列化,适合要完全控制输出节奏的情况。普通 JSON 接口还是用 return 最省心。

32-12

前端调自己的接口有三种常见姿势:

  • $fetch('/api/hello'):通用请求函数,客户端和服务器都能用。在「事件处理函数里」(比如按钮点击)发请求时用它。
  • useFetch('/api/hello'):在
  • useAsyncData + $fetch:想把「取数逻辑」和「请求方式」分开,或要更细的缓存策略时用的底层组合。

一个关键优化:当 useFetch / $fetch在服务器端执行时(比如首屏 SSR),Nuxt 不会真去走一次网络,而是直接调用那个 event handler 函数本体,把结果拿来用。这省了一次 HTTP 往返,也让首屏数据和接口逻辑保持「单一来源」。到浏览器端水合后,后续的客户端请求才真正走网络。所以写接口时记住它会被「两端」调用,别在里面写只能在某一端跑的代码(必要时用 import.meta.server / import.meta.client 判断环境)。

32-13

这一章你学会了 Nuxt 服务端路由的基本功:在 server/api/defineEventHandler 就能出接口;用 getRouterParamgetQueryreadBody 拿参数;用文件名后缀 .get/.post 区分方法;用 createErrorsetResponseStatus 控制错误与状态;公共逻辑放 server/utils/,深层嵌套用 #server 别名。下一章我们把镜头拉远,看看支撑这一切的 Nitro 引擎到底强在哪。

32-6 服务端路由的性能考量

Nitro 引擎处理服务端路由的性能非常高,但写出不高效的代码仍然会影响响应速度。几个值得注意的点:避免在路由处理函数里做耗时的同步操作,如大文件读取或复杂计算;善用缓存,对于不经常变化的数据可以设置缓存头;数据库查询要加索引,避免全表扫描。

另一个常见的性能陷阱是在路由里串行调用多个 API。如果几个数据获取之间没有依赖关系,应该用 Promise.all 并行执行,而不是逐个 await。这能显著减少总响应时间。

对于需要身份验证的路由,建议在中间件层统一处理鉴权逻辑,而不是在每个路由里重复写验证代码。这样既减少了代码冗余,也便于统一修改安全策略。

32-7 错误处理与响应格式

服务端路由里的错误处理直接影响 API 的可靠性。在 Nitro 路由里,未捕获的异常会导致 500 错误返回给客户端。建议在每个路由处理函数里用 try-catch 包裹核心逻辑,捕获异常后返回有意义的错误信息。

Nuxt 提供了 createError 工具函数,方便你创建带有状态码和消息的错误响应。比如当请求的资源不存在时,返回 404 错误;当参数不合法时,返回 400 错误。这些错误会被 Nuxt 的错误处理机制统一处理,客户端能收到格式一致的错误响应。

32-8 接口版本管理的建议

当你的 API 逐渐增多、业务不断迭代时,接口版本管理就变得重要。常见的做法是在 URL 里加版本号,比如 /api/v1/users/api/v2/users。在 Nuxt 里,你可以在 server/api/ 下创建 v1/v2/ 子目录来组织不同版本的接口。

版本管理的好处是:你可以在不破坏旧客户端的情况下引入新接口。移动端 App 的用户更新缓慢,旧版本可能还在调用 v1 的接口。如果直接改了 v1 的行为,这些用户就会出问题。通过保留旧版本、同时提供新版本,给客户端留出迁移时间。

不过,如果你的项目是纯 Web 应用且前后端同步部署,版本管理的紧迫性就没那么高。在这种情况下,保持接口简洁、及时更新就好,不需要过早引入版本管理的复杂度。