进阶篇四 Nuxt4 Server Routes:写后端 API

文章目录

个人网站
前端工程师也能写后端!Nuxt 的 Server Routes 让你不用离开项目就能创建完整的后端 API。今天我们来学习如何用 Nuxt 构建生产级别的 API 服务。

一、创建 API 路由

最简单的 API:

ts 复制代码
// server/api/hello.ts
export default defineEventHandler(() => {
  return {
    message: 'Hello World'
  }
})

访问 GET /api/hello:

json 复制代码
{ "message": "Hello World" }

二、RESTful API

按 HTTP 方法组织:

复制代码
server/api/articles/
├── index.get.ts    # GET    /api/articles
├── index.post.ts   # POST   /api/articles
├── [id].get.ts     # GET    /api/articles/:id
├── [id].put.ts     # PUT    /api/articles/:id
└── [id].delete.ts  # DELETE /api/articles/:id

获取列表

ts 复制代码
// server/api/articles/index.get.ts
interface Article {
  id: number
  title: string
  content: string
  author: string
  createdAt: string
}

// 模拟数据库
const articles: Article[] = [
  { id: 1, title: '文章1', content: '内容1', author: 'Alice', createdAt: '2024-01-01' },
  { id: 2, title: '文章2', content: '内容2', author: 'Bob', createdAt: '2024-01-02' }
]

export default defineEventHandler((event) => {
  const query = getQuery(event)
  const page = Number(query.page) || 1
  const pageSize = Number(query.pageSize) || 10
  
  const start = (page - 1) * pageSize
  const end = start + pageSize
  const list = articles.slice(start, end)
  
  return {
    data: list,
    total: articles.length,
    page,
    pageSize
  }
})

创建文章

ts 复制代码
// server/api/articles/index.post.ts
export default defineEventHandler(async (event) => {
  // 获取请求体
  const body = await readBody(event)
  
  // 验证数据
  if (!body.title || !body.content) {
    throw createError({
      statusCode: 400,
      message: '标题和内容不能为空'
    })
  }
  
  // 创建文章
  const article = {
    id: articles.length + 1,
    title: body.title,
    content: body.content,
    author: body.author || '匿名',
    createdAt: new Date().toISOString()
  }
  
  articles.push(article)
  
  // 返回创建结果
  setResponseStatus(event, 201)
  return article
})

获取详情

ts 复制代码
// server/api/articles/[id].get.ts
export default defineEventHandler((event) => {
  const id = Number(getRouterParam(event, 'id'))
  
  const article = articles.find(a => a.id === id)
  
  if (!article) {
    throw createError({
      statusCode: 404,
      message: '文章不存在'
    })
  }
  
  return article
})

更新文章

ts 复制代码
// server/api/articles/[id].put.ts
export default defineEventHandler(async (event) => {
  const id = Number(getRouterParam(event, 'id'))
  const body = await readBody(event)
  
  const index = articles.findIndex(a => a.id === id)
  
  if (index === -1) {
    throw createError({
      statusCode: 404,
      message: '文章不存在'
    })
  }
  
  articles[index] = {
    ...articles[index],
    ...body
  }
  
  return articles[index]
})

删除文章

ts 复制代码
// server/api/articles/[id].delete.ts
export default defineEventHandler((event) => {
  const id = Number(getRouterParam(event, 'id'))
  
  const index = articles.findIndex(a => a.id === id)
  
  if (index === -1) {
    throw createError({
      statusCode: 404,
      message: '文章不存在'
    })
  }
  
  articles.splice(index, 1)
  
  return { success: true }
})

三、文件上传

ts 复制代码
// server/api/upload.post.ts
import { writeFile } from 'fs/promises'
import { join } from 'path'

export default defineEventHandler(async (event) => {
  const files = await readMultipartFormData(event)
  
  if (!files) {
    throw createError({
      statusCode: 400,
      message: '没有上传文件'
    })
  }
  
  const results = []
  
  for (const file of files) {
    if (!file.filename || !file.data) continue
    
    // 生成文件名
    const ext = file.filename.split('.').pop()
    const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`
    const filepath = join(process.cwd(), 'public/uploads', filename)
    
    // 保存文件
    await writeFile(filepath, file.data)
    
    results.push({
      filename,
      url: `/uploads/${filename}`,
      size: file.data.length
    })
  }
  
  return { files: results }
})

四、认证 API

ts 复制代码
// server/api/auth/login.post.ts
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'

export default defineEventHandler(async (event) => {
  const { email, password } = await readBody(event)
  
  // 查找用户
  const user = users.find(u => u.email === email)
  
  if (!user) {
    throw createError({
      statusCode: 401,
      message: '用户不存在'
    })
  }
  
  // 验证密码
  const valid = await bcrypt.compare(password, user.password)
  
  if (!valid) {
    throw createError({
      statusCode: 401,
      message: '密码错误'
    })
  }
  
  // 生成 token
  const token = jwt.sign(
    { id: user.id, email: user.email },
    process.env.JWT_SECRET || 'secret',
    { expiresIn: '7d' }
  )
  
  // 设置 Cookie
  setCookie(event, 'token', token, {
    httpOnly: true,
    secure: true,
    maxAge: 60 * 60 * 24 * 7
  })
  
  return {
    user: {
      id: user.id,
      email: user.email,
      name: user.name
    },
    token
  }
})

五、代理外部 API

ts 复制代码
// server/api/proxy/[...].ts
export default defineEventHandler(async (event) => {
  const path = event.path.replace('/api/proxy', '')
  const target = 'https://api.example.com'
  
  const response = await $fetch.raw(`${target}${path}`, {
    method: getMethod(event),
    headers: getHeaders(event),
    body: await readBody(event).catch(() => null)
  })
  
  // 转发响应头
  for (const [key, value] of Object.entries(response.headers)) {
    setResponseHeader(event, key, value)
  }
  
  return response._data
})

六、WebSocket 支持

ts 复制代码
// server/api/_ws.ts
export default defineWebSocketHandler({
  open(peer) {
    console.log('连接建立', peer)
  },
  
  message(peer, message) {
    // 广播消息
    peer.send(`收到: ${message}`)
  },
  
  close(peer) {
    console.log('连接关闭', peer)
  }
})

客户端连接:

ts 复制代码
const ws = new WebSocket('ws://localhost:3000/api/_ws')
ws.onmessage = (e) => console.log(e.data)
ws.send('Hello')

七、请求验证

使用 Zod 进行参数验证:

bash 复制代码
pnpm add zod
ts 复制代码
// server/api/articles/index.post.ts
import { z } from 'zod'

const schema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(1),
  author: z.string().optional(),
  tags: z.array(z.string()).optional()
})

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  
  // 验证
  const result = schema.safeParse(body)
  
  if (!result.success) {
    throw createError({
      statusCode: 400,
      message: '参数验证失败',
      data: result.error.flatten()
    })
  }
  
  // 使用验证后的数据
  const article = createArticle(result.data)
  return article
})

八、API 文档

使用 Swagger:

bash 复制代码
pnpm add @nuxtjs/swagger
ts 复制代码
// server/api/articles/index.get.ts
/**
 * @openapi
 * /api/articles:
 *   get:
 *     description: 获取文章列表
 *     parameters:
 *       - name: page
 *         in: query
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: 文章列表
 */
export default defineEventHandler(() => {
  // ...
})

九、Rate Limiting

ts 复制代码
// server/middleware/rate-limit.ts
const requests = new Map<string, number[]>()

export default defineEventHandler((event) => {
  const ip = getRequestIP(event)
  const now = Date.now()
  const window = 60 * 1000
  const max = 100
  
  const timestamps = requests.get(ip) || []
  const recent = timestamps.filter(t => now - t < window)
  
  if (recent.length >= max) {
    throw createError({
      statusCode: 429,
      message: '请求过于频繁'
    })
  }
  
  recent.push(now)
  requests.set(ip, recent)
})

总结

Server Routes 核心:

功能 实现
RESTful API 按方法命名文件
路由参数 [id].ts
请求体 readBody(event)
文件上传 readMultipartFormData(event)
Cookie setCookie / getCookie
错误处理 createError()
缓存 defineCachedEventHandler()

有了 Server Routes,Nuxt 变成了真正的全栈框架。下一篇聊聊部署方案。


有帮助点个赞!评论区见 💪

Server Routes:写后端 API

前端工程师也能写后端!Nuxt 的 Server Routes 让你不用离开项目就能创建完整的后端 API。今天我们来学习如何用 Nuxt 构建生产级别的 API 服务。

一、创建 API 路由

最简单的 API:

ts 复制代码
// server/api/hello.ts
export default defineEventHandler(() => {
  return {
    message: 'Hello World'
  }
})

访问 GET /api/hello:

json 复制代码
{ "message": "Hello World" }

二、RESTful API

按 HTTP 方法组织:

复制代码
server/api/articles/
├── index.get.ts    # GET    /api/articles
├── index.post.ts   # POST   /api/articles
├── [id].get.ts     # GET    /api/articles/:id
├── [id].put.ts     # PUT    /api/articles/:id
└── [id].delete.ts  # DELETE /api/articles/:id

获取列表

ts 复制代码
// server/api/articles/index.get.ts
interface Article {
  id: number
  title: string
  content: string
  author: string
  createdAt: string
}

// 模拟数据库
const articles: Article[] = [
  { id: 1, title: '文章1', content: '内容1', author: 'Alice', createdAt: '2024-01-01' },
  { id: 2, title: '文章2', content: '内容2', author: 'Bob', createdAt: '2024-01-02' }
]

export default defineEventHandler((event) => {
  const query = getQuery(event)
  const page = Number(query.page) || 1
  const pageSize = Number(query.pageSize) || 10
  
  const start = (page - 1) * pageSize
  const end = start + pageSize
  const list = articles.slice(start, end)
  
  return {
    data: list,
    total: articles.length,
    page,
    pageSize
  }
})

创建文章

ts 复制代码
// server/api/articles/index.post.ts
export default defineEventHandler(async (event) => {
  // 获取请求体
  const body = await readBody(event)
  
  // 验证数据
  if (!body.title || !body.content) {
    throw createError({
      statusCode: 400,
      message: '标题和内容不能为空'
    })
  }
  
  // 创建文章
  const article = {
    id: articles.length + 1,
    title: body.title,
    content: body.content,
    author: body.author || '匿名',
    createdAt: new Date().toISOString()
  }
  
  articles.push(article)
  
  // 返回创建结果
  setResponseStatus(event, 201)
  return article
})

获取详情

ts 复制代码
// server/api/articles/[id].get.ts
export default defineEventHandler((event) => {
  const id = Number(getRouterParam(event, 'id'))
  
  const article = articles.find(a => a.id === id)
  
  if (!article) {
    throw createError({
      statusCode: 404,
      message: '文章不存在'
    })
  }
  
  return article
})

更新文章

ts 复制代码
// server/api/articles/[id].put.ts
export default defineEventHandler(async (event) => {
  const id = Number(getRouterParam(event, 'id'))
  const body = await readBody(event)
  
  const index = articles.findIndex(a => a.id === id)
  
  if (index === -1) {
    throw createError({
      statusCode: 404,
      message: '文章不存在'
    })
  }
  
  articles[index] = {
    ...articles[index],
    ...body
  }
  
  return articles[index]
})

删除文章

ts 复制代码
// server/api/articles/[id].delete.ts
export default defineEventHandler((event) => {
  const id = Number(getRouterParam(event, 'id'))
  
  const index = articles.findIndex(a => a.id === id)
  
  if (index === -1) {
    throw createError({
      statusCode: 404,
      message: '文章不存在'
    })
  }
  
  articles.splice(index, 1)
  
  return { success: true }
})

三、文件上传

ts 复制代码
// server/api/upload.post.ts
import { writeFile } from 'fs/promises'
import { join } from 'path'

export default defineEventHandler(async (event) => {
  const files = await readMultipartFormData(event)
  
  if (!files) {
    throw createError({
      statusCode: 400,
      message: '没有上传文件'
    })
  }
  
  const results = []
  
  for (const file of files) {
    if (!file.filename || !file.data) continue
    
    // 生成文件名
    const ext = file.filename.split('.').pop()
    const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`
    const filepath = join(process.cwd(), 'public/uploads', filename)
    
    // 保存文件
    await writeFile(filepath, file.data)
    
    results.push({
      filename,
      url: `/uploads/${filename}`,
      size: file.data.length
    })
  }
  
  return { files: results }
})

四、认证 API

ts 复制代码
// server/api/auth/login.post.ts
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'

export default defineEventHandler(async (event) => {
  const { email, password } = await readBody(event)
  
  // 查找用户
  const user = users.find(u => u.email === email)
  
  if (!user) {
    throw createError({
      statusCode: 401,
      message: '用户不存在'
    })
  }
  
  // 验证密码
  const valid = await bcrypt.compare(password, user.password)
  
  if (!valid) {
    throw createError({
      statusCode: 401,
      message: '密码错误'
    })
  }
  
  // 生成 token
  const token = jwt.sign(
    { id: user.id, email: user.email },
    process.env.JWT_SECRET || 'secret',
    { expiresIn: '7d' }
  )
  
  // 设置 Cookie
  setCookie(event, 'token', token, {
    httpOnly: true,
    secure: true,
    maxAge: 60 * 60 * 24 * 7
  })
  
  return {
    user: {
      id: user.id,
      email: user.email,
      name: user.name
    },
    token
  }
})

五、代理外部 API

ts 复制代码
// server/api/proxy/[...].ts
export default defineEventHandler(async (event) => {
  const path = event.path.replace('/api/proxy', '')
  const target = 'https://api.example.com'
  
  const response = await $fetch.raw(`${target}${path}`, {
    method: getMethod(event),
    headers: getHeaders(event),
    body: await readBody(event).catch(() => null)
  })
  
  // 转发响应头
  for (const [key, value] of Object.entries(response.headers)) {
    setResponseHeader(event, key, value)
  }
  
  return response._data
})

六、WebSocket 支持

ts 复制代码
// server/api/_ws.ts
export default defineWebSocketHandler({
  open(peer) {
    console.log('连接建立', peer)
  },
  
  message(peer, message) {
    // 广播消息
    peer.send(`收到: ${message}`)
  },
  
  close(peer) {
    console.log('连接关闭', peer)
  }
})

客户端连接:

ts 复制代码
const ws = new WebSocket('ws://localhost:3000/api/_ws')
ws.onmessage = (e) => console.log(e.data)
ws.send('Hello')

七、请求验证

使用 Zod 进行参数验证:

bash 复制代码
pnpm add zod
ts 复制代码
// server/api/articles/index.post.ts
import { z } from 'zod'

const schema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(1),
  author: z.string().optional(),
  tags: z.array(z.string()).optional()
})

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  
  // 验证
  const result = schema.safeParse(body)
  
  if (!result.success) {
    throw createError({
      statusCode: 400,
      message: '参数验证失败',
      data: result.error.flatten()
    })
  }
  
  // 使用验证后的数据
  const article = createArticle(result.data)
  return article
})

八、API 文档

使用 Swagger:

bash 复制代码
pnpm add @nuxtjs/swagger
ts 复制代码
// server/api/articles/index.get.ts
/**
 * @openapi
 * /api/articles:
 *   get:
 *     description: 获取文章列表
 *     parameters:
 *       - name: page
 *         in: query
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: 文章列表
 */
export default defineEventHandler(() => {
  // ...
})

九、Rate Limiting

ts 复制代码
// server/middleware/rate-limit.ts
const requests = new Map<string, number[]>()

export default defineEventHandler((event) => {
  const ip = getRequestIP(event)
  const now = Date.now()
  const window = 60 * 1000
  const max = 100
  
  const timestamps = requests.get(ip) || []
  const recent = timestamps.filter(t => now - t < window)
  
  if (recent.length >= max) {
    throw createError({
      statusCode: 429,
      message: '请求过于频繁'
    })
  }
  
  recent.push(now)
  requests.set(ip, recent)
})

总结

Server Routes 核心:

功能 实现
RESTful API 按方法命名文件
路由参数 [id].ts
请求体 readBody(event)
文件上传 readMultipartFormData(event)
Cookie setCookie / getCookie
错误处理 createError()
缓存 defineCachedEventHandler()

有了 Server Routes,Nuxt 变成了真正的全栈框架。下一篇聊聊部署方案。

相关文章

入门篇三:Nuxt4组件自动导入:写代码少敲一半字

入门篇二:Nuxt 4路由自动生成:告别手动配置路由的日子

延伸阅读

nuxt4完整系列,持续更新中。。,欢迎来逛逛


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

相关推荐
寂寞狼牙冷4 分钟前
封装fetch替换axios
前端
用户4611696572820 分钟前
网站 HTTPS 不稳定?证书排查指南
前端
n876531 分钟前
Playwright Java WEB UI 自动化实现框架与逻辑
java·前端·ui
keyipatience33 分钟前
TypeScript学习(1)
前端·javascript·学习·typescript·html·css3
xifangge20251 小时前
基于 JavaScript 实现 CNC 数控切削参数与螺纹公差纯前端核算引擎
开发语言·前端·javascript
wangruofeng1 小时前
2026 Web 技术选型指南,AI 时代如何选择适合项目的技术栈
前端·aigc·ai编程
honkun62 小时前
vue 表格组件 vxe-table 修改滚动条样式,修改滚动条样式,统一宽度、颜色与自动隐藏
前端·javascript·vue.js
爱勇宝2 小时前
研究数字孪生的这半年,做了个 Demo,也有了一些新想法
前端·程序员
不可能片场2 小时前
工程实践 一个缓存把选片从十分钟压到一分钟
前端·electron
前端小万2 小时前
探索副业五个月,最终以放弃告终
前端·后端·面试