进阶篇四 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完整系列,持续更新中。。,欢迎来逛逛


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

相关推荐
brien920 分钟前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance35 分钟前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M42 分钟前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写44 分钟前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李1 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马1 小时前
前沿视界
前端
kyriewen1 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易2 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒2 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端