Skip to content

响应构建

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 操作

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 防护
    })
})
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'
    })
})
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)
})

这是一个第三方 Farrow 文档站 | 用 ❤️ 和 TypeScript 构建