学习nuxtjs,不仅可以写前端,还能直接写后端接口
简单的项目,使用nuxtjs直接前后端一把梭了
所有的后端代码都存放在根目录的server/文件夹中。
Nuxt 会自动扫描server/ 目录下的文件并生成对应的 API 路由:
| 目录 | 描述 | 访问路径 |
|---|---|---|
server/api/ |
放置 API 接口文件 | localhost:3000/api/xxx |
server/routes/ |
放置普通路由文件(不带 /api 前缀) | localhost:3000/xxx |
server/middleware/ |
服务端中间件(注意:这和前端中间件不同) | 每次服务端请求都会 |
api文件
在server/api/hello.ts中写入:
export default defineEventHandler((event) => {
// event 包含了请求的所有信息(headers, context, etc.)
return {
message: '你好,这是来自 Nuxt 服务器的数据!',
time: new Date().toISOString()
}
})
访问方式:打开浏览器输入http://localhost:3000/api/hello,你会直接看到 JSON。
需要注意,我们之前写了/api的代理,现在测试会看不到后端接口,解决办法就是代理路径写具体一点
文件夹嵌套的情况
server/
└── api/
└── v1/
├── users.ts // 访问路径: /api/v1/users
└── products/
└── list.ts // 访问路径: /api/v1/products/list
动态路由参数
嵌套中经常配合中括号使用,用来捕获 URL 中的动态片段。
单参数:server/api/user/[id].ts→ 访问/api/user/123
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id') // 获取路径中的 id
return { userId: id }
})
全匹配 (Catch-all):server/api/cms/[...slug].ts→ 访问/api/cms/a/b/c
export default defineEventHandler((event) => {
const slug = getRouterParam(event, 'slug') // 结果为 "a/b/c"
return { path: slug }
})
获取请求参数
文件:server/api/user.ts
export default defineEventHandler((event) => {
const query = getQuery(event) // 自动解析 ?id=123&name=tom
return { id: query.id }
})
文件:server/api/login.post.ts(注意文件名里的.post后缀,这限制了只有 POST 能访问)
export default defineEventHandler(async (event) => {
const body = await readBody(event) // 获取 JSON body
return { status: 'success', data: body }
})
不同的请求方式
Nuxt 通过文件名后缀来区分不同的 HTTP 动词。如果不写后缀,默认匹配所有方式(GET, POST, PUT, DELETE 等)。
// server/api/order.get.ts
export default defineEventHandler(() => "查询订单列表")
// server/api/order.post.ts
export default defineEventHandler(() => "创建新订单")
大写敏感:虽然 HTTP 标准里 Method 是大写的,但在 Nuxt 文件名后缀中请保持小写(如
.post.ts而非.POST.ts)。
响应流与 Server-Sent Events (SSE)
如果你要做 AI 聊天机器人(流式输出),Nuxt 的 API 是支持发送流的。
// server/api/chat.post.ts
export default defineEventHandler(async (event) => {
// 1. 设置响应头,告诉浏览器这是一个 SSE 流
setResponseHeaders(event, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
})
const responseText = "你好!我是 Nuxt 机器人。我可以实现流式输出,就像真的 AI 一样。有什么我可以帮你的吗?"
const words = responseText.split('')
// 2. 获取底层的 Node.js 响应对象
const res = event.node.res
// 3. 模拟流式发送
for (const word of words) {
// SSE 格式要求以 "data: " 开头,以 "\n\n" 结尾
res.write(`data: ${JSON.stringify({ message: word })}\n\n`)
// 模拟异步延迟
await new Promise(resolve => setTimeout(resolve, 100))
}
// 4. 传输结束
res.end()
})
在前端,我们不能简单的await useFetch,因为那会等到所有数据传输完才返回。我们需要使用fetch结合ReadableStream。
<template>
<div class="p-10">
<button @click="sendChat" class="bg-blue-500 text-white p-2 rounded">开始对话</button>
<div class="mt-4 p-4 border rounded bg-gray-50 min-h-[100px]">
<p>AI 回复:{{ aiReply }}</p>
</div>
</div>
</template>
<script setup lang="ts">
const aiReply = ref('')
const sendChat = async () => {
aiReply.value = ''
// 使用原生 fetch,因为 $fetch 目前对流式响应的直接支持比较复杂
const response = await fetch('/api/chat', {
method: 'POST',
})
if (!response.body) return
// 1. 获取读取器
const reader = response.body.getReader()
const decoder = new TextDecoder()
// 2. 循环读取流数据
while (true) {
const { done, value } = await reader.read()
if (done) break
// 3. 解码并解析 SSE 格式
const chunk = decoder.decode(value)
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.replace('data: ', '')
try {
const data = JSON.parse(jsonStr)
aiReply.value += data.message // 逐字累加
} catch (e) {
console.error('解析错误', e)
}
}
}
}
}
</script>
路由文件
server/routes/目录下的文件与server/api/的写法完全一致,唯一的区别是:生成的 URL 不带/api** 前缀**。
应用场景
- Webhooks:对接第三方平台(如 GitHub、支付回调)时,对方可能要求特定的路径,不能带
/api。 - RSS 订阅 / 站点地图:生成
sitemap.xml或feed.xml。 - 文件下载:直接提供动态生成的 PDF 或图片流。
在 server/routes/health.ts 中:
export default defineEventHandler(() => {
return "OK"; // 访问路径:http://localhost:3000/health
})
优先级
因为没有前缀,所以这里访问路径可能会和项目的静态文件地址冲突,如果冲突public/路由的优先级高于server/routes/目录。
服务端api特殊之处
- 自动映射
你不需要安装 Express 或 Koa,也不需要写router.get(...)。文件名就是路由名。
server/api/test.ts→/api/testserver/api/auth/[...].ts→/api/auth/*(全匹配路由)
- 类型安全
如果你在前端使用useFetch('/api/hello'),Nuxt 会自动推断出返回值的类型。这就是所谓的Full-stack Type Safety。
Nuxt 基于其背后的服务器引擎Nitro,在开发环境下会自动生成.nuxt/types/nitro.ts 等类型定义文件。
当你定义一个server/api路由时,Nuxt 会通过静态分析提取defineEventHandler返回值的类型。前端的useFetch或$fetch 是泛型函数,它们会自动寻找并匹配对应的路径类型。
后端定义:server/api/user/[id].ts
// 后端逻辑
export default defineEventHandler((event) => {
return {
id: 1,
username: '阿强',
age: 25,
tags: ['Vue', 'Nuxt', 'Nitro']
}
})
前端使用,当你输入以下代码时,TypeScript 会自动推断出user的结构:
<script setup lang="ts">
// 这里不需要手动定义 Interface!
// data 会被自动推断为:Ref<{ id: number, username: string, age: number, tags: string[] } | null>
const { data: user } = await useFetch('/api/user/123')
// 魔法发生在这里:
console.log(user.value?.username) // 自动补全:username, age, tags
console.log(user.value?.address) // ❌ 报错:Property 'address' does not exist
</script>
如果是动态路由,需要用模板字符串
const route = useRoute()
// 只要你的模板字符串格式符合 server/api/user/[id].ts 的模式
const { data } = await useFetch(`/api/user/${route.params.id}`)
- 运行时环境
这些代码运行在 Nitro 引擎上。这意味着它们不仅可以跑在 Node.js 环境,还可以无缝部署到 Cloudflare Workers、Vercel Edge 等边缘计算平台。
服务端中间件
注意!这和我们之前讲的页面中间件(Route Middleware)完全不同。
- 位置:
server/middleware/log.ts - 作用:拦截所有发往服务器的请求(包括静态资源、API 请求)。
- 场景:打日志、给所有请求添加自定义 Header、处理跨域。
// server/middleware/log.ts
export default defineEventHandler((event) => {
console.log('新请求来了: ' + getRequestURL(event))
})
如果你在中间件里return了任何非undefined的内容,Nuxt 会直接终止请求并把这个内容发给浏览器
执行顺序
Nuxt 是根据server/middleware/目录下的文件名字母顺序来决定谁先运行的。
01.auth.ts会比02.log.ts先执行。- 建议在文件名前加数字前缀,明确控制拦截流。
event.context(跨文件传值)
这是中间件最有用的场景:解密 Token 并透传给后续 API。
// server/middleware/auth.ts
export default defineEventHandler((event) => {
const user = { id: 123, name: 'admin' } // 假设这是从 Token 解出的用户信息
event.context.user = user // 把用户信息挂载到上下文
})
// server/api/me.ts
export default defineEventHandler((event) => {
// 在后续的 API 里直接拿,不用重复校验 Token
return { currentUser: event.context.user }
})
utils工具
Nuxt 会自动扫描server/utils/目录并自动导入其中的函数。
- 文件:
server/utils/db.ts
export const formatUser = (user: any) => ({ ...user, formatted: true })
- **使用:**在任何
server/api/*.ts中直接使用formatUser(),无需import。
如果不同文件里面有相同的函数,nuxt会采用后发先至的原则,不过最好在创建之前就避免这个问题,或者使用显式导入,或者使用命名空间
使用命名空间
// server/utils/user.ts
export const UserUtils = {
format: (user: any) => ({ ...user, type: 'user' })
}
// server/utils/admin.ts
export const AdminUtils = {
format: (admin: any) => ({ ...admin, type: 'admin' })
}
显式导入
// server/api/test.ts
import { formatUser as formatMember } from '../utils/member'
import { formatUser as formatGuest } from '../utils/guest'
export default defineEventHandler(() => {
// 这里可以安全地使用别名
})