Nuxt server/ 目录深度解析:API 路由、中间件、插件与 h3/Nitro 底层实现
本文围绕 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 实例,可以在其中挂接 request、render 等钩子、注册运行时钩子(如模板中声明的 dev:ssr-logs、render:html 等,见 nitroSchemaTemplate),是扩展服务端行为最灵活的位置。
服务端工具函数
服务端路由由 h3(h3js/h3 项目)驱动,h3 自带一整套请求处理辅助函数(getRequestURL、getQuery、readBody、sendRedirect 等),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 请求中使用 readBody,readBody 会抛出 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-encoding、connection、keep-alive、upgrade、expect、host、accept。
在响应后等待 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/ 目录的真实用例与自动化测试,可用于对照验证上述行为:
- 测试夹具 test/fixtures/basic/server:包含
api/(hello.ts、counter.ts、random.ts、按方法命名的 hey/index.get.ts 与hey/index.post.ts)、plugins/headers.ts(服务端插件实例)与 routes/proxy.ts(非/api前缀路由)。 - 端到端测试 test/basic.test.ts 覆盖了这些服务端路由的实际请求行为。
#server别名写入 Nitro tsconfig paths 的行为由 load-nuxt.test.ts 验证。- 上下文隔离(app/server 不可互相导入)由 import-protection.ts 插件与 import-protection 测试 保证。
掌握以上内容后,你就具备了在 Nuxt 中完整设计服务端层的能力:用 server/api 组织 RESTful 端点、用 server/middleware 与 server/plugins 处理横切逻辑、用 server/types 与 #server 别名维护类型与导入一致性,并理解这一切如何经由 @nuxt/nitro-server 构建器与 h3 落地到最终运行时。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00