Skip to content

server/ 目录

server/ 目录包含了所有服务端代码,由 Nitro 引擎驱动。这是 Nuxt 成为"全栈框架"的关键——你不需要单独搭建后端项目。

为什么 server/ 目录重要?

如果你只用过 Vue 等前端框架,你可能会觉得"后端是后端的事,和我无关"。但在 Nuxt 中:

你想做的事不用 server/用 server/
从数据库获取数据需要单独的后端服务直接在 server/api/ 写接口
用户登录验证依赖第三方服务自己写认证逻辑
保护 API 密钥暴露在前端代码中(任何人都能看到)在服务端使用,客户端看不到
处理文件上传需要额外的文件服务器直接处理
定时任务需要单独的服务Nitro 支持

核心理念

app/ 里写的是用户能在浏览器里看到和交互的代码,server/ 里写的是用户看不到的、运行在服务器上的代码。两者在一个项目里,但运行环境完全不同。

目录结构

text
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/hello
  • routes/ 下的文件,URL 没有前缀。如 routes/sitemap.ts/sitemap
  • 99% 的情况用 api/ 就够了,routes/ 通常用于 sitemap、RSS 等非 API 响应

server/api/ — API 路由

自动添加 /api 前缀,使用 defineEventHandler 定义处理函数:

ts
// 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 的文件命名约定:

ts
// 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.tsGET获取用户列表
users.post.tsPOST创建新用户
users/[id].put.tsPUT更新用户
users/[id].delete.tsDELETE删除用户

如果一个文件没有方法后缀(如 users.ts),它匹配所有 HTTP 方法。

动态路由参数

ts
// 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 前缀

ts
// 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/ — 服务中间件

每个请求到达路由前都会先经过中间件:

ts
// 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 插件

服务端启动时执行一次,用于初始化:

ts
// 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  // 把数据库实例挂到请求上下文上
  })
})

为什么要把数据库连接放在插件里?

  1. 避免每次请求都创建新连接(连接创建很慢)
  2. 在插件中创建一次,所有请求共享同一个连接池
  3. 通过 event.context 传递给后续的 API 处理函数

Nitro 插件 vs Nuxt 插件(app/plugins/

  • Nitro 插件:仅在服务端执行,用于初始化数据库、注册钩子等
  • Nuxt 插件:在服务端和客户端都可能执行,用于 Vue 应用的全局设置

server/utils/ — 工具函数

自动导入的服务端工具函数:

ts
// server/utils/database.ts
export const createDbConnection = () => {
  // 数据库连接逻辑
}

server/ 中的任何地方直接使用,无需 import。

自动导入的范围

  • server/utils/ 的函数 → 在 server/ 下所有文件中自动可用
  • app/utils/ 的函数 → 在 app/ 下所有文件中自动可用
  • shared/utils/ 的函数 → 两边都自动可用

INFO

server/utils/ 的函数不能在客户端代码中使用 反之亦然。如果你有前后端都要用的工具,放在 shared/utils/

#server 别名

可以使用 #server 别名替代相对路径导入:

ts
// 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(...) 抛出错误。

基于 Nuxt 4 官方文档整理编写