文章目录
- [Server Routes:写后端 API](#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 变成了真正的全栈框架。下一篇聊聊部署方案。
相关文章
延伸阅读
内容有帮助?点赞、收藏、关注三连!评论区等你 💪