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


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

相关推荐
萧行之1 小时前
解决Microsoft Edge/Hotmail登录报错(15/25/2603、0x80190001)
前端·microsoft·edge
Eiceblue1 小时前
C# 删除 PDF 页面:单页 / 多页批量删除技巧
前端·pdf·c#
悟空瞎说1 小时前
从isMounted到跨页面状态:高级前端如何优雅解决订单场景的“幽灵陷阱”(附React/Vue完整代码)
前端·javascript
C_fashionCat1 小时前
【2026面试题】前端实际场景去考察原理
前端·vue.js·面试
落魄江湖行2 小时前
进阶篇三 Nuxt4 Nitro 引擎:Nuxt 的服务端核心
前端·vue.js·typescript·nuxt4
sheeta19982 小时前
TypeScript references 配置与 emit 要求详解
javascript·ubuntu·typescript
一壶纱2 小时前
Element Plus 主题构建方案
前端·vue.js
程序员马晓博2 小时前
我的大脑不下班:一个前端工程师的工作反刍自救指南
前端
吴声子夜歌2 小时前
Vue3——表单元素绑定
前端·vue·es6