响应构建
farrow-http 提供了优雅的链式 API 来构建各种类型的响应。
Response 链式调用
farrow-http 的 Response 就像搭乐高,一块一块组装:
typescript
// JSON 响应
Response.json({ message: 'Success' })
// 带状态码
Response.status(201).json({ id: 1 })
// 带 Header
Response
.json({ data: [] })
.status(200)
.header('X-Total-Count', '100')
.header('Cache-Control', 'max-age=3600')
// 设置 Cookie
Response
.json({ success: true })
.cookie('sessionId', 'abc123', {
httpOnly: true,
secure: true,
maxAge: 86400000 // 24 小时
})各种响应类型
JSON 响应
typescript
// 基础 JSON
app.get('/users').use(() => {
return Response.json({ users: [] })
})
// 带状态码
app.post('/users').use((req) => {
const user = createUser(req.body)
return Response.status(201).json(user)
})
// 带自定义 headers
app.get('/api/data').use(() => {
return Response
.json({ data: [] })
.header('X-API-Version', 'v1')
.header('X-Total-Count', '100')
})文本响应
typescript
// 纯文本
app.get('/health').use(() => {
return Response.text('OK')
})
// 带状态码
app.get('/error').use(() => {
return Response.status(500).text('Internal Server Error')
})HTML 响应
typescript
app.get('/').use(() => {
return Response.html('<h1>欢迎</h1>')
})
app.get('/page').use(() => {
const html = `
<!DOCTYPE html>
<html>
<head><title>My Page</title></head>
<body>
<h1>Hello World</h1>
</body>
</html>
`
return Response.html(html)
})文件响应
typescript
// 发送文件
app.get('/download').use(() => {
return Response.file('./files/document.pdf')
})
// 带下载文件名
app.get('/report').use(() => {
return Response
.file('./reports/monthly.pdf')
.attachment('月度报告.pdf')
})
// 带缓存控制
app.get('/image').use(() => {
return Response
.file('./images/logo.png')
.header('Cache-Control', 'public, max-age=3600')
})重定向
typescript
// 基础重定向 (302)
app.get('/old-path').use(() => {
return Response.redirect('/new-path')
})
// 永久重定向 (301)
app.get('/old-url').use(() => {
return Response.status(301).redirect('/new-url')
})
// 重定向到外部 URL
app.get('/external').use(() => {
return Response.redirect('https://example.com')
})
// 临时重定向 (307)
app.post('/submit').use(() => {
return Response.status(307).redirect('/thank-you')
})空响应
typescript
// 204 No Content
app.delete('/users/<id:int>').use((req) => {
deleteUser(req.params.id)
return Response.status(204).empty()
})
// 200 空响应
app.head('/users/<id:int>').use((req) => {
const exists = userExists(req.params.id)
return exists
? Response.status(200).empty()
: Response.status(404).empty()
})流式响应
typescript
import { Readable } from 'stream'
// 文本流
app.get('/stream').use(() => {
const stream = Readable.from(['Hello', ' ', 'World'])
return Response.stream(stream)
})
// 文件流
app.get('/large-file').use(() => {
const stream = fs.createReadStream('./large-file.dat')
return Response
.stream(stream)
.header('Content-Type', 'application/octet-stream')
})
// Server-Sent Events
app.get('/events').use(() => {
const stream = new Readable({
read() {}
})
// 定期发送事件
const interval = setInterval(() => {
stream.push(`data: ${JSON.stringify({ time: Date.now() })}\n\n`)
}, 1000)
// 清理
stream.on('close', () => {
clearInterval(interval)
})
return Response
.stream(stream)
.header('Content-Type', 'text/event-stream')
.header('Cache-Control', 'no-cache')
.header('Connection', 'keep-alive')
})状态码
常用状态码
typescript
// 2xx 成功
Response.status(200).json({ success: true }) // OK
Response.status(201).json({ id: 123 }) // Created
Response.status(204).empty() // No Content
// 3xx 重定向
Response.status(301).redirect('/new-url') // Moved Permanently
Response.status(302).redirect('/temp-url') // Found (Temporary)
Response.status(304).empty() // Not Modified
// 4xx 客户端错误
Response.status(400).json({ error: 'Bad Request' })
Response.status(401).json({ error: 'Unauthorized' })
Response.status(403).json({ error: 'Forbidden' })
Response.status(404).json({ error: 'Not Found' })
Response.status(409).json({ error: 'Conflict' })
Response.status(422).json({ error: 'Unprocessable Entity' })
// 5xx 服务器错误
Response.status(500).json({ error: 'Internal Server Error' })
Response.status(502).json({ error: 'Bad Gateway' })
Response.status(503).json({ error: 'Service Unavailable' })语义化状态码
typescript
// 成功创建资源
app.post('/users').use((req) => {
const user = createUser(req.body)
return Response.status(201).json(user)
})
// 成功删除资源
app.delete('/users/<id:int>').use((req) => {
deleteUser(req.params.id)
return Response.status(204).empty()
})
// 资源冲突
app.post('/users').use(async (req) => {
const existing = await findUserByEmail(req.body.email)
if (existing) {
return Response.status(409).json({
error: 'Email already exists'
})
}
const user = await createUser(req.body)
return Response.status(201).json(user)
})
// 条件请求
app.get('/users/<id:int>').use((req) => {
const user = getUser(req.params.id)
const etag = generateEtag(user)
if (req.headers['if-none-match'] === etag) {
return Response.status(304).empty()
}
return Response
.json(user)
.header('ETag', etag)
})Headers 操作
设置单个 Header
typescript
app.get('/api/data').use(() => {
return Response
.json({ data: [] })
.header('X-API-Version', 'v1')
})设置多个 Headers
typescript
app.get('/api/data').use(() => {
return Response
.json({ data: [] })
.header('X-API-Version', 'v1')
.header('X-Request-ID', generateId())
.header('X-Response-Time', `${Date.now()}ms`)
})常用 Headers
typescript
// CORS
app.get('/api/data').use(() => {
return Response
.json({ data: [] })
.header('Access-Control-Allow-Origin', '*')
.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
})
// 缓存控制
app.get('/static/image').use(() => {
return Response
.file('./image.png')
.header('Cache-Control', 'public, max-age=86400') // 1 天
.header('ETag', generateEtag())
})
// 安全相关
app.get('/').use(() => {
return Response
.html('<h1>Hello</h1>')
.header('X-Frame-Options', 'DENY')
.header('X-Content-Type-Options', 'nosniff')
.header('X-XSS-Protection', '1; mode=block')
})
// 内容类型
app.get('/data').use(() => {
return Response
.text('Hello')
.header('Content-Type', 'text/plain; charset=utf-8')
})Cookies 操作
设置 Cookie
typescript
app.post('/login').use((req) => {
const { username, password } = req.body
const token = authenticate(username, password)
return Response
.json({ success: true })
.cookie('token', token, {
httpOnly: true, // 防止 XSS
secure: true, // 仅 HTTPS
maxAge: 86400000, // 24 小时 (毫秒)
sameSite: 'strict' // CSRF 防护
})
})Cookie 选项
typescript
interface CookieOptions {
domain?: string // Cookie 域名
path?: string // Cookie 路径
maxAge?: number // 有效期 (毫秒)
expires?: Date // 过期时间
httpOnly?: boolean // 仅 HTTP 访问
secure?: boolean // 仅 HTTPS
sameSite?: 'strict' | 'lax' | 'none' // SameSite 策略
}
// 使用示例
app.get('/set-cookie').use(() => {
return Response
.json({ success: true })
.cookie('session', 'abc123', {
domain: 'example.com',
path: '/',
maxAge: 3600000, // 1 小时
httpOnly: true,
secure: true,
sameSite: 'lax'
})
})删除 Cookie
typescript
app.post('/logout').use(() => {
return Response
.json({ success: true })
.cookie('token', '', {
maxAge: 0 // 立即过期
})
})静态文件服务
typescript
// 基础静态文件服务
app.serve('/static', './public')
// 多个静态目录
app.serve('/css', './public/css')
app.serve('/js', './public/js')
app.serve('/images', './public/images')
// 上传文件目录
app.serve('/uploads', './storage/uploads')内置安全特性:
- ✅ 自动防止目录遍历攻击
- ✅ 自动文件权限检查
- ✅ 目录请求自动返回 index.html
- ✅ 跨平台路径处理
URL 映射:
/static/style.css → ./public/style.css
/static/ → ./public/index.html
/uploads/../secret → 被阻止 (安全防护)⚠️ Response 合并陷阱
重要: Response.merge 遵循"后者覆盖前者"原则!
typescript
// ❌ 错误: JSON 被空 body 覆盖
Response.json({ users: [] })
.merge(Response.header('X-Version', 'v1'))
// 结果: 只有空 body 和 header,JSON 数据丢失!
// ✅ 正确: 使用链式调用
Response.json({ users: [] })
.header('X-Version', 'v1')
// 结果: 有 JSON body 和 header
// ✅ 或者确保 body 在最后
Response.header('X-Version', 'v1')
.merge(Response.json({ users: [] }))
// 结果: 有 JSON body 和 header响应拦截
全局响应拦截
typescript
// 为所有 JSON 响应添加时间戳
app.capture('json', (jsonBody) => {
return Response.json({
data: jsonBody.value,
timestamp: new Date().toISOString(),
version: 'v1'
})
})
// 为所有文件响应添加缓存头
app.capture('file', (fileBody) => {
return Response.file(fileBody.value, fileBody.options)
.header('Cache-Control', 'public, max-age=3600')
})响应转换
typescript
// 统一包装 API 响应
app.capture('json', (jsonBody) => {
return Response.json({
success: true,
data: jsonBody.value,
meta: {
timestamp: Date.now(),
version: 'v1.0'
}
})
})
// 使用
app.get('/users').use(() => {
return Response.json({ users: [] })
})
// 实际响应:
// {
// "success": true,
// "data": { "users": [] },
// "meta": {
// "timestamp": 1234567890,
// "version": "v1.0"
// }
// }实用模式
模式 1: API 响应包装器
typescript
interface APIResponse<T> {
success: boolean
data?: T
error?: string
meta: {
timestamp: number
requestId: string
}
}
function apiSuccess<T>(data: T) {
return Response.json({
success: true,
data,
meta: {
timestamp: Date.now(),
requestId: RequestIdContext.get()
}
})
}
function apiError(message: string, status = 400) {
return Response.status(status).json({
success: false,
error: message,
meta: {
timestamp: Date.now(),
requestId: RequestIdContext.get()
}
})
}
// 使用
app.get('/users/<id:int>').use((req) => {
const user = getUser(req.params.id)
if (!user) {
return apiError('User not found', 404)
}
return apiSuccess(user)
})模式 2: 分页响应
typescript
interface PaginatedResponse<T> {
data: T[]
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
function paginatedResponse<T>(
data: T[],
page: number,
limit: number,
total: number
) {
return Response.json({
data,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
})
}
// 使用
app.get('/users?<page?:int>&<limit?:int>').use((req) => {
const { page = 1, limit = 10 } = req.query
const users = getUsers(page, limit)
const total = getTotalUsers()
return paginatedResponse(users, page, limit, total)
})模式 3: 条件响应
typescript
app.get('/data').use((req) => {
const acceptHeader = req.headers.accept
const data = getData()
if (acceptHeader?.includes('application/json')) {
return Response.json(data)
}
if (acceptHeader?.includes('text/html')) {
return Response.html(`<pre>${JSON.stringify(data, null, 2)}</pre>`)
}
if (acceptHeader?.includes('text/plain')) {
return Response.text(JSON.stringify(data))
}
// 默认返回 JSON
return Response.json(data)
})