server/ 目录
server/ 目录包含了所有服务端代码,由 Nitro 引擎驱动。这是 Nuxt 成为"全栈框架"的关键——你不需要单独搭建后端项目。
为什么 server/ 目录重要?
如果你只用过 Vue 等前端框架,你可能会觉得"后端是后端的事,和我无关"。但在 Nuxt 中:
| 你想做的事 | 不用 server/ | 用 server/ |
|---|---|---|
| 从数据库获取数据 | 需要单独的后端服务 | 直接在 server/api/ 写接口 |
| 用户登录验证 | 依赖第三方服务 | 自己写认证逻辑 |
| 保护 API 密钥 | 暴露在前端代码中(任何人都能看到) | 在服务端使用,客户端看不到 |
| 处理文件上传 | 需要额外的文件服务器 | 直接处理 |
| 定时任务 | 需要单独的服务 | Nitro 支持 |
核心理念
app/ 里写的是用户能在浏览器里看到和交互的代码,server/ 里写的是用户看不到的、运行在服务器上的代码。两者在一个项目里,但运行环境完全不同。
目录结构
server/
├── api/ # API 路由(自动 /api 前缀)
│ ├── hello.ts → /api/hello
│ └── users/
│ ├── index.get.ts → GET /api/users
│ └── [id].get.ts → GET /api/users/:id
├── routes/ # 服务路由(无 /api 前缀)
│ └── sitemap.ts → /sitemap.xml
├── middleware/ # 服务中间件(每个请求执行)
│ └── auth.ts
├── plugins/ # Nitro 插件(启动时执行一次)
│ └── db.ts
├── utils/ # 服务端工具函数(自动导入)
│ └── database.ts
└── tsconfig.json # 服务端 TypeScript 配置(自动生成)api/ 和 routes/ 的区别
api/下的文件,URL 自动加/api前缀。如api/hello.ts→/api/helloroutes/下的文件,URL 没有前缀。如routes/sitemap.ts→/sitemap- 99% 的情况用
api/就够了,routes/通常用于 sitemap、RSS 等非 API 响应
server/api/ — API 路由
自动添加 /api 前缀,使用 defineEventHandler 定义处理函数:
// server/api/hello.ts
export default defineEventHandler(() => {
return { message: 'Hello World!' }
})defineEventHandler 做了什么?
- 它是 Nitro(Nuxt 的服务端引擎)提供的辅助函数
- 为你提供 TypeScript 类型提示
- 返回的对象自动序列化为 JSON
- 可以省略不写,但推荐写上,有类型提示
访问 GET /api/hello 返回 { "message": "Hello World!" }
HTTP 方法匹配
文件名后缀匹配 HTTP 方法——这是 Nuxt/Nitro 的文件命名约定:
// server/api/users.get.ts → 仅匹配 GET 请求
export default defineEventHandler(() => {
return { users: [] }
})
// server/api/users.post.ts → 仅匹配 POST 请求
export default defineEventHandler(async (event) => {
const body = await readBody(event)
return { created: true }
})为什么要按 HTTP 方法拆分文件?
以前你可能在一个文件里写 if (method === 'GET') ... else if (method === 'POST') ...,代码又长又难维护。Nuxt 的约定是:一个文件只处理一种方法,简洁清晰。
| 文件名 | 匹配的方法 | 典型用途 |
|---|---|---|
users.get.ts | GET | 获取用户列表 |
users.post.ts | POST | 创建新用户 |
users/[id].put.ts | PUT | 更新用户 |
users/[id].delete.ts | DELETE | 删除用户 |
如果一个文件没有方法后缀(如 users.ts),它匹配所有 HTTP 方法。
动态路由参数
// server/api/users/[id].ts
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id')
return { id, name: `User ${id}` }
})getRouterParam vs 直接解析 URL?
getRouterParam 是 Nitro 提供的类型安全方法,它从路由匹配结果中取参数,而不是自己解析 URL。推荐使用。
server/routes/ — 服务路由
与 api/ 类似,但不带 /api 前缀:
// server/routes/sitemap.ts → 访问 /sitemap
export default defineEventHandler(() => {
return '<?xml version="1.0" encoding="UTF-8"?><urlset>...</urlset>'
})典型场景
/sitemap.xml— 站点地图/robots.txt— 爬虫规则/rss.xml— RSS 订阅/manifest.json— PWA 清单
这些都不是 API 接口,不需要 /api 前缀,放在 routes/ 更合理。
server/middleware/ — 服务中间件
每个请求到达路由前都会先经过中间件:
// server/middleware/log.ts
export default defineEventHandler((event) => {
console.log(`[${event.method}] ${getRequestURL(event)}`)
})服务中间件 vs 路由中间件的区别
| 特性 | 服务中间件(server/middleware/) | 路由中间件(app/middleware/) |
|---|---|---|
| 运行位置 | 服务端 | 客户端 + 服务端 |
| 触发时机 | 每个服务端请求 | 每次路由跳转 |
| 典型用途 | 日志、CORS、限流 | 登录检查、权限验证 |
| 能否返回响应 | 不应该 | 可以(abortNavigation) |
INFO
️ 中间件不应该返回响应 仅用于检查和扩展请求上下文。如果你需要拦截某些请求并返回错误,应该在路由处理函数中做
server/plugins/ — Nitro 插件
服务端启动时执行一次,用于初始化:
// server/plugins/db.ts
import { DrizzleDB } from '#server/utils/database'
export default defineNitroPlugin(async (nitroApp) => {
const db = await DrizzleDB.create()
nitroApp.hooks.hook('request', (event) => {
event.context.db = db // 把数据库实例挂到请求上下文上
})
})为什么要把数据库连接放在插件里?
- 避免每次请求都创建新连接(连接创建很慢)
- 在插件中创建一次,所有请求共享同一个连接池
- 通过
event.context传递给后续的 API 处理函数
Nitro 插件 vs Nuxt 插件(app/plugins/)
- Nitro 插件:仅在服务端执行,用于初始化数据库、注册钩子等
- Nuxt 插件:在服务端和客户端都可能执行,用于 Vue 应用的全局设置
server/utils/ — 工具函数
自动导入的服务端工具函数:
// server/utils/database.ts
export const createDbConnection = () => {
// 数据库连接逻辑
}在 server/ 中的任何地方直接使用,无需 import。
自动导入的范围
server/utils/的函数 → 在server/下所有文件中自动可用app/utils/的函数 → 在app/下所有文件中自动可用shared/utils/的函数 → 两边都自动可用
INFO
️ server/utils/ 的函数不能在客户端代码中使用 反之亦然。如果你有前后端都要用的工具,放在 shared/utils/
#server 别名
可以使用 #server 别名替代相对路径导入:
// server/api/deep/nested/route.ts
import { helper } from '#server/utils/helper' // 替代 ../../../utils/helper为什么需要 #server?
当你的 API 文件嵌套很深时,用 ../../../ 很容易出错。#server 始终指向 server/ 目录根路径,不受文件位置影响。
INFO
️ #server 只能在 server/ 目录内使用
服务端类型隔离
Nuxt 4 为 server/ 目录生成独立的 tsconfig.json,确保类型准确:
- 服务端代码不会自动导入客户端类型
- 客户端代码不会自动导入服务端类型
shared/目录的类型两边都可使用
为什么需要类型隔离?
没有隔离时,你可能不小心在客户端代码中调用了 readBody()(服务端 API),或者在服务端代码中调用了 useState()(客户端 API),TypeScript 不会报错,但运行时会出错。
类型隔离让这些错误在编码阶段就能被发现。
常见问题
在客户端代码中引用 server/utils 的函数报错
这是正常的!server/utils/ 的函数只在服务端可用。如果前后端都要用,把函数移到 shared/utils/。
API 返回的不是 JSON
确保你 return 的是对象或数组,Nitro 会自动序列化为 JSON。如果你返回字符串,它会作为纯文本响应。
中间件中返回了响应但请求没有中断
服务中间件不应该返回响应。如果你需要拦截请求,使用 throw createError(...) 抛出错误。