首页
/ Nuxt server/ 目录深度解析:API 路由、中间件、插件与 h3/Nitro 底层实现

Nuxt server/ 目录深度解析:API 路由、中间件、插件与 h3/Nitro 底层实现

2026-09-04 10:25:19作者:范垣楠Rhoda

本文围绕 Nuxt 的 server/ 目录展开,系统讲解如何在该目录下声明式地注册 API 路由、普通服务端路由、中间件、插件与工具函数,覆盖路由参数、HTTP 方法匹配、Catch-all、请求体/查询参数解析、错误与状态码处理、运行时配置、响应后异步任务等全部实战场景,并结合 Nuxt 源码(构建入口、导入保护、#server 别名解析)解释每个机制背后的实现原理,帮助你既会写 Nuxt 服务端代码,也理解它与 Nitro、h3 的协作方式。

server/ 目录是什么

server/ 目录用于向 Nuxt 应用注册 API 和服务端处理器。Nuxt 会自动扫描这些目录中的文件,将其注册为 API 与服务端路由,并支持热模块替换(HMR)——开发时修改服务端文件无需重启进程即可生效。

标准目录结构如下:

- server/
  - api/
    - hello.ts      # 注册为 /api/hello
  - routes/
    - bonjour.ts    # 注册为 /bonjour
  - middleware/
    - log.ts        # 记录所有请求日志

核心规则:每个文件都应导出一个用 defineEventHandler()(或其别名 eventHandler())定义的默认函数。Handler 可以直接返回 JSON 数据、一个 Promise,或一个 Response 对象。

重要约束:不要在服务端路由或工具函数中导入 Vue 应用代码(组件、composables 或其他仅属于应用的工具),也不要在应用代码中导入仅属于服务端的代码。服务端运行在 Nitro(Node.js/边缘运行时)上下文,与 Vue 应用是完全隔离的模块图,混用会导致构建失败或运行时错误。

构建层面的支撑:bundleServer 与 Nitro Server 构建器

从源码结构看,服务端目录的打包由 server 构建入口 负责。bundleServer(nuxt) 会根据 nuxt.options.server.builder 动态加载构建器,默认值为 @nuxt/nitro-server(见 loadServerBuilder 的默认参数):

// packages/nuxt/src/core/server.ts(节选)
export async function bundleServer (nuxt: Nuxt) {
  const { bundle } = !nuxt.options.server.builder || typeof nuxt.options.server.builder === 'string'
    ? await loadServerBuilder(nuxt, nuxt.options.server.builder)
    : nuxt.options.server.builder
  await bundle(nuxt)
}

这说明 server/ 目录的所有扫描、路由注册与最终打包,实际上都委托给了 Nitro 构建器完成——这也是 Nuxt 服务端代码可以直接使用 Nitro 全部能力(存储、缓存、任务调度、运行时钩子)的原因。

第一个 API 路由

最简单的服务端路由如下:

import { defineEventHandler } from 'nitro/h3'

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

此后可以在任何页面或组件中通用调用该 API:

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

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

useFetch 在客户端发起请求,在服务端则直接内联到页面数据抓取中(SSR 场景下不会产生额外的客户端往返)。

导入保护:为什么不能从 app 直接 import server 代码

上面的“不要混用”约束在 Nuxt 中有硬性的编译期实现。import-protection 插件 会对 Vue 应用上下文(nuxt-app)与 #shared 共享上下文注册两条保护规则:

// packages/nuxt/src/core/plugins/import-protection.ts(节选)
patterns.push([
  new RegExp('^' + serverRelative + '\\/(api|routes|middleware|plugins)\\/'),
  `Importing from server is not allowed in ${context}.`,
  ['Use `$fetch()` or `useFetch()` to fetch data from server routes.', 'Move shared logic to the `shared/` directory.'],
])
patterns.push([
  /^#server(\/|$)/,
  `Server aliases are not allowed in ${context}.`,
  ['Use `$fetch()` or `useFetch()` to call server endpoints.', 'Move shared logic to the `shared/` directory.'],
])

也就是说:在应用代码中直接 import 相对路径的 server/api/**server/routes/** 等文件,或使用 #server 别名,都会在构建时收到明确报错,报错信息会指引你改用 $fetch()/useFetch(),或把公共逻辑移到 shared/ 目录。反向的规则同样存在(服务端代码中禁止使用 #app#build 等 Vue 应用别名),保证两个上下文彻底隔离。

Server 路由:去掉 /api 前缀

server/api 中的文件会自动加 /api 路由前缀。如果希望注册不带 /api 前缀的服务端路由,把文件放进 server/routes 目录即可:

export default defineEventHandler(() => 'Hello World!')

上述示例中,/hello 路由可以在 http://localhost:3000/hello 访问。

注意:目前服务端路由的动态路由能力尚不如 pages 那样完整,复杂的路径参数场景仍建议放在 server/api 下使用参数文件命名。

服务端中间件

Nuxt 会自动读取 server/middleware 中的每个文件,将其注册为服务端中间件。中间件 Handler 在任何其他服务端路由之前、针对每个请求执行,用于添加或检查请求头、记录日志,或扩展事件的请求上下文。

注意:中间件 Handler 不应返回任何内容(也不应关闭或响应请求),它只能检查或扩展请求上下文,或者抛出错误。

示例: 记录所有请求日志

export default defineEventHandler((event) => {
  console.log('New request: ' + getRequestURL(event))
})

示例: 向请求上下文注入认证信息

export default defineEventHandler((event) => {
  event.context.auth = { user: 123 }
})

注入 event.context.auth 之后,后续任意路由 Handler 都可以通过 event.context.auth 读取该信息——这是把公共鉴权逻辑与业务路由解耦的标准做法。

服务端插件

Nuxt 会自动读取 server/plugins 目录下的所有文件,并注册为 Nitro 插件。这允许你扩展 Nitro 的运行时行为、挂接到生命周期事件上。

import { definePlugin } from 'nitro'

export default definePlugin((nitroApp) => {
  console.log('Nitro plugin', nitroApp)
})

Nitro 插件拿到的是完整的 nitroApp 实例,可以在其中挂接 requestrender 等钩子、注册运行时钩子(如模板中声明的 dev:ssr-logsrender:html 等,见 nitroSchemaTemplate),是扩展服务端行为最灵活的位置。

服务端工具函数

服务端路由由 h3(h3js/h3 项目)驱动,h3 自带一整套请求处理辅助函数(getRequestURLgetQueryreadBodysendRedirect 等),Nuxt 通过 nitro/h3 导出统一提供。

你也可以在 server/utils 目录中添加自己的辅助函数。例如定义一个包裹原始 Handler 的自定义工具,在返回最终响应前后执行额外操作:

export const defineWrappedResponseHandler = <T extends EventHandlerRequest, D> (
  handler: EventHandler<T, D>,
): EventHandler<T, D> =>
  defineEventHandler<T>(async (event) => {
    try {
      // do something before the route handler
      const response = await handler(event)
      // do something after the route handler
      return { response }
    } catch (err) {
      // Error handling
      return { err }
    }
  })
export default defineWrappedResponseHandler(event => 'hello world')

这个模式适合做统一的响应包装、审计日志、限流计数等横切逻辑,避免在每个路由里重复样板代码。

#server 别名(v4.3+)

#server 别名允许从 server/ 目录内的任意位置导入文件,而不受导入文件所在目录深度的限制:

// 不再需要这种相对路径:
// import { formatUser } from '../../../utils/formatUser'

// 直接使用 #server 别名:
import { formatUser } from '#server/utils/formatUser'

该别名能保证服务端代码导入路径的一致性,在深层嵌套的路由 Handler 中尤其有用。

源码层面如何工作: 别名在配置解析阶段注入。common 配置alias.$resolve 会生成:

'#shared': withTrailingSlash(resolve(rootDir, sharedDir)),
'#server': withTrailingSlash(serverDir),

并且该别名会同步写入 Nitro 的 tsconfig paths,load-nuxt 测试 验证了生成结果中同时包含 #server#server/* 两条映射,保证编辑器类型提示与运行时解析一致。

限制#server 别名只能在 server/ 目录内部使用。在客户端代码中从 #server 导入会触发前面提到的导入保护报错("Server aliases are not allowed in the Vue part of your app")。

服务端类型

server/ 目录与 app/ 目录运行在不同上下文,二者的自动导入与类型系统互不相同。Nuxt 4 默认生成的 tsconfig.json 中包含一个覆盖 server/ 文件夹的项目引用(project reference),确保服务端代码获得准确的类型推断。

放在 server/types/ 中的类型只会在服务端上下文自动导入——你可以在服务端路由、中间件、插件和工具函数中直接引用,无需显式 import。而那些同时需要用在 Vue 应用中的类型,应当放进 shared/types/

export interface Todo {
  id: string
  title: string
  completed: boolean
}
import { defineEventHandler } from 'nitro/h3'

export default defineEventHandler((): Todo[] => {
  return []
})

shared/types/ 的扫描规则一致:只有直接位于 server/types/ 下的文件会被扫描,嵌套子目录中的文件不会被自动导入。

实用配方

路由参数

服务端路由支持在文件名的方括号中写动态参数,如 server/api/hello/[name].ts,参数可通过 event.context.params 访问:

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

  return `Hello, ${name}!`
})

提示:可以改用 getValidatedRouterParams 配合 Zod 或 Valibot 等 schema 校验器,同时获得运行时校验与类型安全。

现在可以通用调用 /api/hello/nuxt 这个 API,得到 Hello, nuxt!

匹配 HTTP 方法

Handler 文件名可以追加 .get.post.put.delete 等后缀,用于匹配请求的 HTTP 方法:

export default defineEventHandler(() => 'Test get handler')
export default defineEventHandler(() => 'Test post handler')

对上述示例,请求 /test 时:

  • GET 方法:返回 Test get handler
  • POST 方法:返回 Test post handler
  • 其他方法:返回 405 错误

你也可以在目录中使用 index.[method].ts 来组织代码,便于创建 API 命名空间:

export default defineEventHandler((event) => {
  // 处理 api/foo 端点的 GET 请求
})
export default defineEventHandler((event) => {
  // 处理 api/foo 端点的 POST 请求
})
export default defineEventHandler((event) => {
  // 处理 api/foo/bar 端点的 GET 请求
})

这种 index.get.ts + index.post.ts 的写法在测试夹具中也有真实用例,例如 test/fixtures/basic/server/api/hey 下就按此模式组织了 GET/POST 两个 Handler。

捕获全部(Catch-all)路由

Catch-all 路由适合做兜底处理。例如创建文件 server/api/foo/[...].ts,会为所有未匹配到任何路由 Handler 的请求注册一个捕获全部路由,如 /api/foo/bar/baz

export default defineEventHandler((event) => {
  // event.context.path 获取路由路径:'/api/foo/bar/baz'
  // event.context.params._ 获取路由片段:'bar/baz'
  return `Default foo handler`
})

使用 server/api/foo/[...slug].ts 可以为捕获段命名,并通过 event.context.params.slug 访问:

export default defineEventHandler((event) => {
  // event.context.params.slug 获取路由片段:'bar/baz'
  return `Default foo handler`
})

请求体处理

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

提示:可以改用 readValidatedBody 配合 Zod 或 Valibot 做运行时与类型安全的双重校验。

在前端这样调用:

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

注意:文件名中使用 submit.post.ts 只是为了匹配可接收请求体的 POST 请求。如果在 GET 请求中使用 readBodyreadBody 会抛出 405 Method Not Allowed HTTP 错误。

查询参数

以查询 /api/query?foo=bar&baz=qux 为例:

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

  return { a: query.foo, b: query.baz }
})

提示:getValidatedQuery 同样支持 schema 校验器,获得运行时与类型安全。

错误处理

如果没有抛出错误,将返回 200 OK 状态码。任何未捕获的错误都会返回 500 Internal Server Error

要返回其他错误码,使用 createError 抛出异常:

export default defineEventHandler((event) => {
  const id = Number.parseInt(event.context.params.id) as number

  if (!Number.isInteger(id)) {
    throw createError({
      status: 400,
      statusText: 'ID should be an integer',
    })
  }
  return 'All good'
})

状态码

要返回其他状态码,使用 setResponseStatus 工具函数。例如返回 202 Accepted

export default defineEventHandler((event) => {
  setResponseStatus(event, 202)
})

运行时配置

服务端路由可以直接读取 Nuxt 运行时配置。以 server/api/foo.ts 为例:

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig()

  const repo = await $fetch('https://api.github.com/repos/nuxt/nuxt', {
    headers: {
      Authorization: `token ${config.githubToken}`,
    },
  })

  return repo
})

配合 nuxt.config.ts.env 文件:

export default defineNuxtConfig({
  runtimeConfig: {
    githubToken: '',
  },
})
NUXT_GITHUB_TOKEN='<my-super-token>'

环境变量前缀 NUXT_ 会将 NUXT_GITHUB_TOKEN 注入到 runtimeConfig.githubToken。密钥类配置建议放在 privateRuntimeConfig 中,避免暴露到客户端。

请求 Cookie

export default defineEventHandler((event) => {
  const cookies = parseCookies(event)

  return { cookies }
})

转发上下文与请求头

默认情况下,在服务端路由中发起 fetch 请求时,既不会转发传入请求的头,也不会转发请求上下文。可以使用 event.$fetch 在发起 fetch 请求时转发请求上下文与头:

export default defineEventHandler((event) => {
  return event.$fetch('/api/forwarded')
})

注意:不适合被转发的头不会被包含在请求中,例如 transfer-encodingconnectionkeep-aliveupgradeexpecthostaccept

在响应后等待 Promise(waitUntil)

处理服务端请求时,你可能需要执行不应阻塞客户端响应的异步任务(例如缓存写入、日志上报)。可以使用 event.waitUntil 在后台等待一个 Promise,而不延迟响应。

event.waitUntil 方法接收一个 Promise,它会在 Handler 结束前被等待完成,确保即使服务端在响应发出后立刻结束 Handler,任务也依然会执行完成。该方法与运行时 Provider 集成,利用各平台原生的“响应后异步任务”能力。

const timeConsumingBackgroundTask = async () => {
  await new Promise(resolve => setTimeout(resolve, 1000))
}

export default eventHandler((event) => {
  // 调度后台任务,不阻塞响应
  event.waitUntil(timeConsumingBackgroundTask())

  // 立即向客户端发送响应
  return 'done'
})

高级用法

Nitro 配置

可以在 nuxt.config 中使用 nitro 键直接设置 Nitro 配置。

警告:这是一个高级选项。自定义配置可能影响生产部署——当 Nitro 在 Nuxt 的 semver-minor 版本中升级时,配置接口可能会随时间变化。相关概念可参考 服务端引擎章节

export default defineNuxtConfig({
  // Nitro 配置文档见 nitro.build/config
  nitro: {},
})

嵌套 Router

在服务端路由内部还可以再创建一层 h3 Router,实现嵌套路由:

import { createRouter, defineEventHandler, useBase } from 'h3'

const router = createRouter()

router.get('/test', defineEventHandler(() => 'Hello World'))

export default useBase('/api/hello', router.handler)

发送流

这是一个实验性特性,在所有环境中可用:

import fs from 'node:fs'
import { sendStream } from 'h3'

export default defineEventHandler((event) => {
  return sendStream(event, fs.createReadStream('/path/to/file'))
})

发送重定向

export default defineEventHandler(async (event) => {
  await sendRedirect(event, '/path/redirect/to', 302)
})

遗留 Handler 或中间件

export default fromNodeMiddleware((req, res) => {
  res.end('Legacy handler')
})

遗留支持可以通过 h3 实现,但建议尽可能避免使用遗留 Handler。

export default fromNodeMiddleware((req, res, next) => {
  console.log('Legacy middleware')
  next()
})

警告:永远不要将 next() 回调与 async 或返回 Promise 的遗留中间件组合使用。

服务端存储

Nitro 提供了一个跨平台的存储层。要配置额外的存储挂载点,可以使用 nitro.storage,或通过服务端插件。

添加 Redis 存储的示例——使用 nitro.storage

export default defineNuxtConfig({
  nitro: {
    storage: {
      redis: {
        driver: 'redis',
        /* redis 连接选项 */
        port: 6379, // Redis 端口
        host: '127.0.0.1', // Redis 主机
        username: '', // 需要 Redis >= 6
        password: '',
        db: 0, // 默认为 0
        tls: {}, // tls/ssl
      },
    },
  },
})

然后在 API Handler 中使用:

export default defineEventHandler(async (event) => {
  // 列出所有 key
  const keys = await useStorage('redis').getKeys()

  // 设置一个 key
  await useStorage('redis').setItem('foo', 'bar')

  // 删除一个 key
  await useStorage('redis').removeItem('foo')

  return {}
})

另一种方式:通过服务端插件 + 运行时配置创建存储挂载点,这样凭据可以从运行时配置动态注入:

import { definePlugin } from 'nitro'
import redisDriver from 'unstorage/drivers/redis'

export default definePlugin(() => {
  const storage = useStorage()

  // 从运行时配置或其他来源动态传入凭据
  const driver = redisDriver({
    base: 'redis',
    host: useRuntimeConfig().redis.host,
    port: useRuntimeConfig().redis.port,
    /* 其他 redis 连接选项 */
  })

  // 挂载驱动
  storage.mount('redis', driver)
})
export default defineNuxtConfig({
  runtimeConfig: {
    redis: { // 默认值
      host: '',
      port: 0,
      /* 其他 redis 连接选项 */
    },
  },
})

验证与延伸阅读

本仓库自带了大量 server/ 目录的真实用例与自动化测试,可用于对照验证上述行为:

掌握以上内容后,你就具备了在 Nuxt 中完整设计服务端层的能力:用 server/api 组织 RESTful 端点、用 server/middlewareserver/plugins 处理横切逻辑、用 server/types#server 别名维护类型与导入一致性,并理解这一切如何经由 @nuxt/nitro-server 构建器与 h3 落地到最终运行时。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384