nuxt服务端API
学习nuxtjs,不仅可以写前端,还能直接写后端接口 简单的项目,使用nuxtjs直接前后端一把梭了 所有的后端代码都存放在根目录的server/文件夹中。 Nuxt 会自动扫描server/

nuxt服务端API

发布时间:2026-04-21 (1小时前)

学习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.xmlfeed.xml
  • 文件下载:直接提供动态生成的 PDF 或图片流。

在 server/routes/health.ts 中:

export default defineEventHandler(() => {
  return "OK"; // 访问路径:http://localhost:3000/health
})

优先级

因为没有前缀,所以这里访问路径可能会和项目的静态文件地址冲突,如果冲突public/路由的优先级高于server/routes/目录。

服务端api特殊之处

  1. 自动映射

你不需要安装 Express 或 Koa,也不需要写router.get(...)文件名就是路由名

  • server/api/test.ts/api/test
  • server/api/auth/[...].ts/api/auth/* (全匹配路由)
  1. 类型安全

如果你在前端使用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}`)
  1. 运行时环境

这些代码运行在 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(() => {
  // 这里可以安全地使用别名
})