Skip to content

farrow-http 完整 API 参考 ​

目录 ​

  1. 核心概念
  2. 完整导出清单
  3. HTTP 服务器
  4. 路由系统
  5. 请求与验证
  6. 响应构建
  7. 中间件系统
  8. 上下文管理
  9. 错误处理
  10. 工具函数
  11. 类型定义
  12. 最佳实践

核心概念 ​

设计哲学 ​

farrow-http 是一个类型驱动的 Web 框架,核心设计理念:

  1. 类型安全优先 - 编译期类型检查,运行时自动验证
  2. 声明式路由 - URL 模式自动推导 TypeScript 类型
  3. 函数式中间件 - 基于 farrow-pipeline 的洋葱模型
  4. 自动验证 - 集成 farrow-schema 的运行时验证
  5. 上下文隔离 - 基于 AsyncLocalStorage 的请求级隔离

三大支柱 ​

farrow-pipeline (中间件管道)  →  处理流程控制
farrow-schema   (类型验证)     →  自动验证和类型推导
farrow-http     (HTTP 框架)    →  路由和响应构建

执行模型 ​

typescript
// 洋葱模型执行顺序
app.use((req, next) => {
  console.log('1: 进入')       // 1
  const res = next(req)
  console.log('4: 退出')       // 4
  return res
})

app.use((req, next) => {
  console.log('2: 进入')       // 2
  const res = next(req)
  console.log('3: 退出')       // 3
  return res
})

自动异步追踪 ​

farrow-http 会在启动时自动启用异步追踪,无需手动配置:

typescript
import { Http } from 'farrow-http'

// 框架内部自动调用 asyncTracerImpl.enable()
const app = Http()
// ✅ Context 在异步操作中自动传递

完整导出清单 ​

主入口 (farrow-http) ​

typescript
// HTTP 服务器
export { Http, Https }
export type { HttpPipeline, HttpsPipeline, HttpPipelineOptions, HttpsPipelineOptions }

// 路由系统
export { Router }
export type {
  RouterPipeline,
  RoutingMethods,
  RoutingMethod,
  RouterUrlSchema,
  RouterRequestSchema,
  RouterSharedSchema,
  MatchOptions,
  HttpMiddleware,
  HttpMiddlewareInput,
  Pathname
}

// 请求信息
export type {
  RequestInfo,
  RequestQuery,
  RequestHeaders,
  RequestCookies
}

// 响应构建
export { Response, matchBodyType }
export type { MaybeAsyncResponse }

// 响应信息
export type {
  ResponseInfo,
  Body,
  BodyMap,
  Status,
  Headers,
  Cookies,
  EmptyBody,
  StringBody,
  JsonBody,
  StreamBody,
  BufferBody,
  FileBody,
  RedirectBody,
  CustomBody,
  FileBodyOptions,
  CustomBodyHandler,
  RedirectOptions
}

// 上下文钩子
export {
  useReq,           // 获取 Node.js 请求对象(断言非空)
  useRequest,       // 获取 Node.js 请求对象(可能为空)
  useRequestInfo,   // 获取 farrow 请求信息
  useRes,           // 获取 Node.js 响应对象(断言非空)
  useResponse       // 获取 Node.js 响应对象(可能为空)
}

// Basename 路由
export { useBasenames, usePrefix }

// 错误处理
export { HttpError }

// 日志
export type { LoggerOptions, LoggerEvent }

// 类型工具
export type { ParseUrl, MarkReadOnlyDeep }

配套依赖模块 ​

farrow-http 依赖以下模块,需要单独安装和导入:

farrow-pipeline - 中间件管道系统 ​

typescript
// 从 'farrow-pipeline' 导入(不是从 'farrow-http')
import {
  createContext,        // 创建上下文
  createContainer,      // 创建容器
  useContainer,         // 获取当前容器
  runWithContainer,     // 在指定容器中运行
  type Context,         // 上下文类型
  type Container,       // 容器类型
  type ContextStorage,  // 上下文存储类型
  type Middleware,      // 中间件类型
  type Pipeline,        // 同步管道类型
  type AsyncPipeline,   // 异步管道类型
  type Next,            // Next 函数类型
  type MaybeAsync       // 可能异步类型
} from 'farrow-pipeline'

farrow-schema - 类型定义和验证 ​

typescript
// 从 'farrow-schema' 导入(不是从 'farrow-http')
import {
  Schema,               // Schema 基类
  ObjectType,           // 对象类型(支持递归)
  StructType,           // 结构体类型(不支持递归)

  // 基础类型
  String, Number, Boolean, Int, Float, ID, Date,

  // 复合类型构造函数
  List, Optional, Nullable, Record, Tuple,

  // 联合与交集
  Union, Intersect, Literal,

  // 结构体构造函数
  Struct,

  // 特殊类型
  Any, Unknown, Never, Json,

  // 工具函数
  TypeOf,               // 提取 TypeScript 类型
  field,                // 定义字段元数据

  // 类型定义
  type FieldDescriptor,
  type FieldDescriptors,
  type SchemaCtor
} from 'farrow-schema'

farrow-schema/validator - 运行时验证 ​

typescript
// 从 'farrow-schema/validator' 导入(不是从 'farrow-http')
import {
  Validator,            // 验证器对象
  createSchemaValidator,// 创建专用验证器
  ValidatorType,        // 自定义验证器基类
  RegExp,               // 正则表达式验证器

  // 类型定义
  type ValidationResult,
  type ValidationError,
  type ValidatorOptions
} from 'farrow-schema/validator'

完整使用示例:

typescript
// 1. 分别导入各个模块
import { Http, Response } from 'farrow-http'
import { createContext } from 'farrow-pipeline'
import { ObjectType, String, Int, Optional } from 'farrow-schema'

// 2. 定义 Schema
class User extends ObjectType {
  name = String
  age = Int
  email = Optional(String)
}

// 3. 创建上下文
const UserContext = createContext<User | null>(null)

// 4. 创建 HTTP 服务器
const app = Http()

// 5. 使用
app.post('/users', { body: User }).use((req) => {
  UserContext.set(req.body)
  return Response.status(201).json(req.body)
})

app.listen(3000)

安装依赖:

bash
npm install farrow-http farrow-pipeline farrow-schema

HTTP 服务器 ​

Http - 创建 HTTP 服务器 ​

函数签名:

typescript
function Http(options?: HttpPipelineOptions): HttpPipeline

类型定义:

typescript
type HttpPipelineOptions = {
  basenames?: string[]                    // 基础路径前缀
  body?: BodyOptions                      // 请求体解析选项(来自 co-body 库)
  cookie?: CookieParseOptions             // Cookie 解析选项(来自 cookie 库)
  query?: IParseOptions                   // 查询参数解析选项(来自 qs 库)
  contexts?: (params: {                   // 上下文注入函数
    req: IncomingMessage
    requestInfo: RequestInfo
    basename: string
  }) => ContextStorage
  logger?: boolean | HttpLoggerOptions    // 日志配置
  errorStack?: boolean                    // 是否显示错误堆栈
}

type HttpLoggerOptions = {
  transporter?: (str: string) => void                        // 自定义日志输出
  ignoreIntrospectionRequestOfFarrowApi?: boolean           // 忽略 farrow-api 内省请求(默认 true)
}

// BodyOptions - 来自 co-body 库
// 用于解析 HTTP 请求体(JSON、表单、文本)
// co-body 是 koa-bodyparser 的底层依赖
type BodyOptions = {
  limit?: string | number          // 请求体大小限制,如 '1mb', '10kb', 1024000
  encoding?: BufferEncoding        // 字符编码,默认 'utf8'
  strict?: boolean                 // JSON 严格模式,默认 true
  jsonTypes?: string[]             // 自定义 JSON content-type 列表,默认 ['application/json']
  formTypes?: string[]             // 自定义表单 content-type 列表,默认 ['application/x-www-form-urlencoded']
  textTypes?: string[]             // 自定义文本 content-type 列表,默认 ['text/plain']
}

// CookieParseOptions - 来自 cookie 库
// 用于解析 HTTP Cookie 头
type CookieParseOptions = {
  decode?: (val: string) => string  // Cookie 值解码函数,默认 decodeURIComponent
}

// IParseOptions - 来自 qs 库
// 用于解析 URL 查询字符串(如 ?a=1&b=2)
type IParseOptions = {
  depth?: number                   // 对象嵌套深度限制,默认 5
  arrayLimit?: number              // 数组元素数量限制,默认 20
  delimiter?: string | RegExp      // 查询参数分隔符,默认 '&'
  allowDots?: boolean              // 是否允许点符号,默认 false(如 ?user.name=Alice)
  parseArrays?: boolean            // 是否解析数组语法,默认 true(如 ?tags[0]=a&tags[1]=b)
  allowPrototypes?: boolean        // 是否允许原型污染,默认 false(安全考虑)
  plainObjects?: boolean           // 是否使用普通对象(无原型),默认 false
  comma?: boolean                  // 是否将逗号视为数组分隔符,默认 false
}

type HttpPipeline = RouterPipeline & {
  handle: (req: IncomingMessage, res: ServerResponse, options?: HttpHandlerOptions) => MaybeAsync<void>
  listen: (...args: Parameters<Server['listen']>) => Server
  server: () => Server
}

基础示例:

typescript
import { Http, Response } from 'farrow-http'

// 创建服务器
const app = Http()

// 添加路由
app.get('/').use(() => Response.json({ message: 'Hello World' }))

// 启动服务器
app.listen(3000, () => console.log('服务器运行在 http://localhost:3000'))

配置示例:

typescript
const app = Http({
  basenames: ['/api/v1'],
  logger: true,

  body: {
    limit: '10mb',
    encoding: 'utf8'
  },

  cookie: {
    decode: decodeURIComponent
  },

  query: {
    depth: 5,
    arrayLimit: 100,
    allowDots: true,
    parseArrays: true
  },

  errorStack: process.env.NODE_ENV === 'development'
})

服务器方法:

listen(port, callback?) ​

启动服务器并监听端口。

typescript
app.listen(3000)
app.listen(3000, () => console.log('服务器启动'))
app.listen(3000, '0.0.0.0', () => console.log('监听所有网卡'))

server() ​

获取底层 Node.js HTTP 服务器实例(用于测试)。

typescript
const server = app.server()  // http.Server 实例

// 用于测试
import request from 'supertest'
const response = await request(server).get('/').expect(200)

handle(req, res, options?) ​

直接处理 Node.js 原生 HTTP 请求(用于集成到其他框架或自定义服务器)。

typescript
import { createServer } from 'http'
import express from 'express'

const app = Http()
app.get('/api/hello').use(() => Response.json({ message: 'Hello' }))

// 方式 1:独立运行(推荐)
app.listen(3000)

// 方式 2:集成到 Express
const expressApp = express()
expressApp.use('/farrow', (req, res) => {
  app.handle(req, res)
})
expressApp.listen(3000)

// 方式 3:自定义服务器
const customServer = createServer(app.handle)
customServer.listen(3000)

说明: app.server() 内部调用 createServer(app.handle)。


Https - 创建 HTTPS 服务器 ​

函数签名:

typescript
function Https(options?: HttpsPipelineOptions): HttpsPipeline

类型定义:

typescript
type HttpsPipelineOptions = HttpPipelineOptions & {
  tls?: SecureContextOptions & TlsOptions
}

type HttpsPipeline = HttpPipeline  // 相同的接口

示例:

typescript
import { Https } from 'farrow-http'
import fs from 'fs'

const app = Https({
  tls: {
    key: fs.readFileSync('private-key.pem'),
    cert: fs.readFileSync('certificate.pem')
  },
  basenames: ['/api'],
  logger: true
})

app.get('/').use(() => Response.json({ secure: true }))

app.listen(443, () => console.log('HTTPS 服务器运行在端口 443'))

路由系统 ​

Router - 创建独立路由器 ​

函数签名:

typescript
function Router(): RouterPipeline

类型定义:

typescript
type RouterPipeline = AsyncPipeline<RequestInfo, Response> & {
  // HTTP 方法路由
  get: RoutingMethod
  post: RoutingMethod
  put: RoutingMethod
  patch: RoutingMethod
  delete: RoutingMethod
  head: RoutingMethod
  options: RoutingMethod

  // 高级匹配
  match<T extends RouterRequestSchema>(
    schema: T,
    options?: MatchOptions
  ): AsyncPipeline<TypeOfRequestSchema<T>, Response>

  match<U extends string, T extends RouterUrlSchema<U>>(
    schema: T,
    options?: MatchOptions
  ): AsyncPipeline<TypeOfUrlSchema<T>, Response>

  // 嵌套路由
  route(name: string): Pipeline<RequestInfo, MaybeAsyncResponse>

  // 静态文件
  serve(name: string, dirname: string): void

  // 响应拦截
  capture<T extends keyof BodyMap>(
    type: T,
    f: (body: BodyMap[T]) => MaybeAsyncResponse
  ): void
}

type RoutingMethod = <U extends string, T extends RouterSharedSchema>(
  path: U,
  schema?: T,
  options?: MatchOptions
) => Pipeline<TypeOfUrlSchema<{ url: U, method: string } & T>, MaybeAsyncResponse>

基础示例:

typescript
import { Router, Response } from 'farrow-http'

const userRouter = Router()

userRouter.get('/').use(() => Response.json({ users: [] }))
userRouter.get('/<id:int>').use((req) => {
  return Response.json({ userId: req.params.id })
})

// 挂载到应用
app.route('/users').use(userRouter)

HTTP 方法路由 ​

所有 HTTP 方法都支持相同的签名:

typescript
type RoutingMethod = <U extends string, T extends RouterSharedSchema>(
  path: U,           // URL 模式
  schema?: T,        // 可选的验证 Schema
  options?: MatchOptions  // 可选的匹配选项
) => Pipeline<...>

get(path, schema?, options?) ​

typescript
// 基础路由
app.get('/').use(() => Response.text('首页'))

// 路径参数
app.get('/user/<id:int>').use((req) => {
  const userId = req.params.id  // number
  return Response.json({ id: userId })
})

// 查询参数
app.get('/search?<q:string>&<page?:int>').use((req) => {
  const { q, page = 1 } = req.query
  return Response.json({ query: q, page })
})

// 带验证的请求头
app.get('/protected', {
  headers: { authorization: String }
}).use((req) => {
  const token = req.headers.authorization
  return Response.json({ authenticated: true })
})

post(path, schema?, options?) ​

typescript
import { ObjectType, String, Int, Optional } from 'farrow-schema'

class CreateUserInput extends ObjectType {
  name = String
  email = String
  age = Optional(Int)
}

app.post('/users', {
  body: CreateUserInput
}).use((req) => {
  // req.body 已验证,类型安全
  const { name, email, age } = req.body
  return Response.status(201).json({ id: Date.now(), name, email, age })
})

put(path, schema?, options?) ​

typescript
class UpdateUserInput extends ObjectType {
  name = Optional(String)
  email = Optional(String)
}

app.put('/users/<id:int>', {
  body: UpdateUserInput
}).use((req) => {
  const { id } = req.params
  const updates = req.body
  return Response.json({ id, ...updates })
})

patch(path, schema?, options?) ​

typescript
app.patch('/users/<id:int>/email', {
  body: { email: String }
}).use((req) => {
  return Response.json({
    id: req.params.id,
    email: req.body.email
  })
})

delete(path, schema?, options?) ​

typescript
app.delete('/users/<id:int>').use((req) => {
  deleteUser(req.params.id)
  return Response.status(204).empty()
})

head(path, schema?, options?) ​

typescript
app.head('/users').use(() => {
  return Response.status(200).empty()
})

options(path, schema?, options?) ​

typescript
app.options('/users').use(() => {
  return Response
    .header('Allow', 'GET, POST, PUT, DELETE')
    .status(200)
    .empty()
})

URL 模式与自动验证 ​

farrow-http 提供强大的 URL 模式系统,支持自动类型推导和验证。

路径参数类型 ​

typescript
// 基础类型
app.get('/user/<id:int>').use((req) => {
  req.params.id  // number
})

app.get('/user/<name:string>').use((req) => {
  req.params.name  // string
})

app.get('/price/<value:float>').use((req) => {
  req.params.value  // number
})

app.get('/active/<flag:boolean>').use((req) => {
  req.params.flag  // boolean
})

app.get('/user/<userId:id>').use((req) => {
  req.params.userId  // string(非空)
})

联合类型 ​

typescript
// 枚举值
app.get('/posts/<status:draft|published|archived>').use((req) => {
  req.params.status  // 'draft' | 'published' | 'archived'
})

// 字面量类型
app.get('/api/<version:{v1}|{v2}>').use((req) => {
  req.params.version  // 'v1' | 'v2'
})

修饰符 ​

typescript
// 可选参数 (?)
app.get('/user/<name?:string>').use((req) => {
  req.params.name  // string | undefined
})

// 一个或多个 (+)
app.get('/tags/<tags+:string>').use((req) => {
  req.params.tags  // string[]
})

// 零个或多个 (*)
app.get('/categories/<cats*:string>').use((req) => {
  req.params.cats  // string[] | undefined
})

查询参数 ​

typescript
// 必需查询参数
app.get('/products?<sort:asc|desc>').use((req) => {
  req.query.sort  // 'asc' | 'desc'
})

// 可选查询参数
app.get('/search?<q:string>&<page?:int>').use((req) => {
  const { q, page = 1 } = req.query
  // q: string, page: number | undefined
})

// 数组查询参数
app.get('/filter?<tags*:string>').use((req) => {
  req.query.tags  // string[] | undefined
})

// 字面量查询参数
app.get('/products?status=active').use((req) => {
  req.query.status  // 'active'
})

组合示例 ​

typescript
app.get('/<category:string>?<search:string>&<page?:int>&<sort?:asc|desc>').use((req) => {
  const { category } = req.params     // string
  const { search, page = 1, sort = 'asc' } = req.query
  // search: string
  // page: number | undefined
  // sort: 'asc' | 'desc' | undefined

  return Response.json({
    category,
    search,
    page,
    sort,
    results: []
  })
})

match - 高级路由匹配 ​

函数签名:

typescript
match<T extends RouterRequestSchema>(
  schema: T,
  options?: MatchOptions
): AsyncPipeline<TypeOfRequestSchema<T>, Response>

match<U extends string, T extends RouterUrlSchema<U>>(
  schema: T,
  options?: MatchOptions
): AsyncPipeline<TypeOfUrlSchema<T>, Response>

类型定义:

typescript
type RouterUrlSchema<T extends string = string> = {
  url: T
  method?: string | string[]
  body?: FieldDescriptor | FieldDescriptors
  headers?: RouterSchemaDescriptor
  cookies?: RouterSchemaDescriptor
}

type RouterRequestSchema = {
  pathname: Pathname
  method?: string | string[]
  params?: RouterSchemaDescriptor
  query?: RouterSchemaDescriptor
  body?: FieldDescriptor | FieldDescriptors
  headers?: RouterSchemaDescriptor
  cookies?: RouterSchemaDescriptor
}

type MatchOptions = {
  block?: boolean                     // 阻塞模式
  onSchemaError?(                     // 验证错误处理
    error: ValidationError,
    input: RequestInfo,
    next: Next<RequestInfo, MaybeAsyncResponse>
  ): MaybeAsyncResponse | void
}

使用示例:

typescript
// URL Schema 方式
app.match({
  url: '/users/<id:int>?<expand?:string>',
  method: ['GET', 'PUT'],
  body: { name: String, email: String },
  headers: { authorization: String }
}, {
  block: true,  // 阻塞模式:验证失败返回 400
  onSchemaError: (error, input, next) => {
    return Response.status(400).json({
      error: '验证失败',
      field: error.path?.join('.'),
      message: error.message,
      received: error.value
    })
  }
}).use((req) => {
  // req.params.id: number
  // req.query.expand: string | undefined
  // req.body: { name: string, email: string }
  // req.headers.authorization: string

  return Response.json({
    id: req.params.id,
    ...req.body
  })
})

// Request Schema 方式
app.match({
  pathname: '/users/:id',
  method: 'POST',
  params: { id: Int },
  body: CreateUserInput,
  headers: { 'content-type': Literal('application/json') }
}).use((req) => {
  return Response.json({ success: true })
})

route - 路径前缀(Basename) ​

函数签名:

typescript
route(name: string): Pipeline<RequestInfo, MaybeAsyncResponse>

说明:

  • 为路径添加前缀(basename),返回的是 Pipeline,不是 RouterPipeline
  • 返回的 Pipeline 没有 get、post 等 HTTP 方法
  • 用于组织路由层级和挂载独立的 Router

基础用法:

typescript
const app = Http({ basenames: ['/api'] })

// ❌ 错误:route() 返回的 Pipeline 没有 HTTP 方法
const v1 = app.route('/v1')
v1.get('/users')  // 错误!Pipeline 没有 get 方法

// ✅ 正确:创建独立 Router 再挂载
const v1Router = Router()
v1Router.get('/users').use(() => Response.json({ users: [] }))
v1Router.get('/posts').use(() => Response.json({ posts: [] }))

app.route('/v1').use(v1Router)

嵌套路由:

typescript
const app = Http({ basenames: ['/api'] })

// 继续嵌套 route
app.route('/v1')
  .route('/users')
  .use((req) => {
    // 请求 /api/v1/users/* 会到这里
    // req.pathname 已经去掉了 /api/v1/users 前缀
    return Response.json({ path: req.pathname })
  })

模块化路由(推荐):

typescript
// users.ts
export const userRouter = Router()

userRouter.get('/').use(() => Response.json({ users: [] }))
userRouter.get('/<id:int>').use((req) => Response.json({ userId: req.params.id }))
userRouter.post('/', { body: CreateUserInput }).use((req) => {
  return Response.status(201).json(createUser(req.body))
})

// posts.ts
export const postRouter = Router()

postRouter.get('/').use(() => Response.json({ posts: [] }))
postRouter.post('/', { body: CreatePostInput }).use((req) => {
  return Response.status(201).json(createPost(req.body))
})

// app.ts
import { userRouter } from './users'
import { postRouter } from './posts'

const app = Http({ basenames: ['/api'] })

// 挂载独立 Router
app.route('/users').use(userRouter)   // GET /api/users
app.route('/posts').use(postRouter)   // GET /api/posts

// 嵌套挂载
const v1Router = Router()
v1Router.route('/users').use(userRouter)  // GET /api/v1/users
v1Router.route('/posts').use(postRouter)  // GET /api/v1/posts

app.route('/v1').use(v1Router)

关键区别:

方法返回类型有 HTTP 方法?用途
Router()RouterPipeline✅ 有创建独立路由器
app.route()Pipeline❌ 没有添加路径前缀

serve - 静态文件服务 ​

函数签名:

typescript
serve(name: string, dirname: string): void

说明: 提供带有内置安全保护的静态文件服务。

内置安全特性:

  • ✅ 自动防止目录遍历攻击(路径规范化)
  • ✅ 自动文件权限检查
  • ✅ 为目录请求提供 index.html
  • ✅ 如果文件未找到,优雅地传递给下一个中间件
  • ✅ 跨平台安全路径处理

示例:

typescript
// 基本静态文件服务
app.serve('/static', './public')
app.serve('/uploads', './storage/uploads')

// URL 映射:
// /static/style.css        → ./public/style.css
// /static/                 → ./public/index.html(如果存在)
// /static/docs/            → ./public/docs/index.html(如果存在)
// /uploads/../secret       → 被阻止(目录遍历防护)

// 组合使用
app.serve('/assets', './public')
app.get('/api/users').use(() => Response.json({ users: [] }))

capture - 响应拦截 ​

函数签名:

typescript
capture<T extends keyof BodyMap>(
  type: T,
  f: (body: BodyMap[T]) => MaybeAsyncResponse
): void

说明: 拦截特定类型的响应体并进行转换。

示例:

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')
    .header('X-File-Server', 'farrow-http')
})

// 处理字符串响应
app.capture('string', (stringBody) => {
  return Response.string(`[${new Date().toISOString()}] ${stringBody.value}`)
})

可捕获的响应类型:

typescript
type BodyMap = {
  empty: EmptyBody
  string: StringBody
  json: JsonBody
  stream: StreamBody
  buffer: BufferBody
  file: FileBody
  redirect: RedirectBody
  custom: CustomBody
}

请求与验证 ​

RequestInfo - 请求信息类型 ​

类型定义:

typescript
type RequestInfo = {
  readonly pathname: string         // 请求路径,如 "/users/123"
  readonly method?: string          // HTTP 方法,如 "GET"
  readonly query?: RequestQuery     // 查询参数
  readonly body?: any               // 请求体(已解析和验证)
  readonly headers?: RequestHeaders // 请求头
  readonly cookies?: RequestCookies // Cookies
  readonly params?: RequestParams   // 路径参数(从 URL 模式解析)
}

type RequestQuery = { readonly [key: string]: any }
type RequestHeaders = { readonly [key: string]: any }
type RequestCookies = { readonly [key: string]: any }
type RequestParams = { readonly [key: string]: any }

特点:

  • ✅ 所有字段都是只读的(readonly)
  • ✅ 路径参数、查询参数、请求体已自动验证和类型转换
  • ✅ 在整个中间件链中保持一致性

请求体验证 ​

使用 ObjectType ​

typescript
import { ObjectType, String, Int, Optional, List } from 'farrow-schema'

class CreateArticleInput extends ObjectType {
  title = String
  content = String
  authorId = Int
  tags = Optional(List(String))
  published = Optional(Boolean)
}

app.post('/articles', {
  body: CreateArticleInput
}).use((req) => {
  // req.body 已验证,类型安全
  const { title, content, authorId, tags = [], published = false } = req.body

  const article = createArticle({
    title,
    content,
    authorId,
    tags,
    published
  })

  return Response.status(201).json(article)
})

使用 FieldDescriptors ​

typescript
app.post('/login', {
  body: {
    username: String,
    password: String,
    remember: Optional(Boolean)
  }
}).use((req) => {
  const { username, password, remember = false } = req.body

  const token = authenticate(username, password)
  return Response.json({ token, remember })
})

嵌套对象验证 ​

typescript
class AddressInput extends ObjectType {
  street = String
  city = String
  zipCode = String
}

class CreateUserInput extends ObjectType {
  name = String
  email = String
  address = AddressInput
  contacts = Optional(List(String))
}

app.post('/users', {
  body: CreateUserInput
}).use((req) => {
  // req.body.address 已验证为 AddressInput
  const { name, email, address, contacts = [] } = req.body

  return Response.status(201).json({
    id: Date.now(),
    name,
    email,
    address,
    contacts
  })
})

自定义验证错误处理 ​

typescript
app.post('/users', {
  body: CreateUserInput
}, {
  onSchemaError: (error, input, next) => {
    // error.message: 错误消息
    // error.path: 字段路径,如 ['body', 'email']
    // error.value: 无效的值

    return Response.status(400).json({
      error: '数据验证失败',
      field: error.path?.join('.'),  // 'body.email'
      message: error.message,
      received: error.value,
      hint: '请检查输入格式'
    })
  }
}).use((req) => {
  return Response.status(201).json(createUser(req.body))
})

请求头和 Cookies 验证 ​

typescript
// 请求头验证
app.get('/protected', {
  headers: {
    authorization: String,
    'x-api-key': Optional(String)
  }
}).use((req) => {
  const token = req.headers.authorization
  const apiKey = req.headers['x-api-key']

  return Response.json({ authenticated: true })
})

// Cookies 验证
app.get('/dashboard', {
  cookies: {
    sessionId: String,
    theme: Optional(Union(Literal('light'), Literal('dark')))
  }
}).use((req) => {
  const { sessionId, theme = 'light' } = req.cookies

  return Response.json({ sessionId, theme })
})

// 组合验证
app.post('/upload', {
  headers: { 'content-type': Literal('multipart/form-data') },
  cookies: { sessionId: String },
  body: UploadFileInput
}).use((req) => {
  return Response.json({ uploaded: true })
})

响应构建 ​

Response - 响应对象 ​

类型定义:

typescript
type Response = {
  info: ResponseInfo                    // 响应信息
  merge: (...responses: Response[]) => Response  // 合并响应
  is: (...types: string[]) => string | false     // 类型检查

  // 响应体方法
  string: (value: string) => Response
  json: (value: JsonType) => Response
  html: (value: string) => Response
  text: (value: string) => Response
  redirect: (url: string, options?: RedirectOptions) => Response
  stream: (stream: Stream) => Response
  file: (filename: string, options?: FileBodyOptions) => Response
  buffer: (buffer: Buffer) => Response
  empty: () => Response
  custom: (handler: CustomBodyHandler) => Response

  // 响应元数据方法
  status: (code: number, message?: string) => Response
  header: (name: string, value: Value) => Response
  headers: (headers: Headers) => Response
  cookie: (name: string, value: Value | null, options?: CookieOptions) => Response
  cookies: (cookies: Record<string, Value | null>, options?: CookieOptions) => Response
  type: (contentType: string) => Response
  vary: (...fields: string[]) => Response
  attachment: (filename?: string, options?: AttachmentOptions) => Response
}

基础响应类型 ​

JSON 响应 ​

typescript
// 简单 JSON
Response.json({ message: 'Hello' })

// 带状态码
Response.status(201).json({ id: 1, name: 'Alice' })

// 带标头
Response.header('X-Total-Count', '100').json({ items: [] })

// 链式调用
Response
  .status(200)
  .header('Cache-Control', 'max-age=3600')
  .header('X-API-Version', '1.0')
  .json({ data: [] })

文本响应 ​

typescript
Response.text('纯文本响应')
Response.status(404).text('未找到')

HTML 响应 ​

typescript
Response.html('<h1>Hello World</h1>')

Response.status(200).html(`
  <!DOCTYPE html>
  <html>
    <head><title>欢迎</title></head>
    <body><h1>欢迎来到 farrow-http</h1></body>
  </html>
`)

文件响应 ​

typescript
// 基本文件响应
Response.file('./uploads/document.pdf')

// 带内容类型
Response.file('./images/logo.png', 'image/png')

// 带流控制选项
Response.file('./large-file.zip', {
  start: 0,
  end: 1024 * 1024 - 1,  // 只读取前 1MB
  highWaterMark: 1024 * 1024  // 1MB 缓冲区
})

// 作为下载附件
Response.file('./report.pdf')
  .attachment('monthly-report.pdf')
  .header('Cache-Control', 'private, max-age=3600')

// 中文文件名带后备
Response.file('./数据报告.xlsx')
  .attachment('数据报告.xlsx', {
    fallback: 'data-report.xlsx'  // 兼容旧浏览器
  })

// 内联显示(浏览器中打开而非下载)
Response.file('./document.pdf')
  .attachment('document.pdf', {
    type: 'inline'
  })

流响应 ​

typescript
import { Readable } from 'stream'

const stream = Readable.from(['Hello', ' ', 'World'])
Response.stream(stream)

// 从文件创建流
import fs from 'fs'
const fileStream = fs.createReadStream('./large-file.log')
Response.stream(fileStream)
  .header('Content-Type', 'text/plain')

重定向响应 ​

typescript
// 基本重定向
Response.redirect('/login')
Response.redirect('https://example.com')

// 不使用路由前缀
Response.redirect('/path', { usePrefix: false })

// 在嵌套路由中的行为
const apiRouter = app.route('/api')
apiRouter.use(() => {
  return Response.redirect('/users')           // 重定向到 /api/users
})
apiRouter.use(() => {
  return Response.redirect('/users', { usePrefix: false })  // 重定向到 /users
})

// 重定向到 referer
Response.redirect('back')  // 重定向到 HTTP Referer 或 '/'

空响应 ​

typescript
Response.status(204).empty()
Response.status(304).empty()  // Not Modified

缓冲区响应 ​

typescript
const buffer = Buffer.from('binary data')
Response.buffer(buffer)
  .header('Content-Type', 'application/octet-stream')

自定义响应 ​

typescript
Response.custom(({ req, res, requestInfo, responseInfo }) => {
  // req: IncomingMessage - Node.js 请求对象
  // res: ServerResponse - Node.js 响应对象
  // requestInfo: RequestInfo - farrow 请求信息
  // responseInfo: Omit<ResponseInfo, 'body'> - farrow 响应信息(无 body)

  res.statusCode = 200
  res.setHeader('Content-Type', 'application/octet-stream')
  res.end(Buffer.from('binary data'))
})

// 服务器推送事件(SSE)
Response.custom(({ req, res }) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  })

  const sendEvent = (data: any) => {
    res.write(`data: ${JSON.stringify(data)}\n\n`)
  }

  sendEvent({ message: '已连接' })

  const interval = setInterval(() => {
    sendEvent({ timestamp: Date.now() })
  }, 1000)

  req.on('close', () => clearInterval(interval))
})

响应方法(可链式调用) ​

状态和标头 ​

typescript
// 设置状态码
Response.status(code: number, message?: string): Response

Response.status(200)
Response.status(404, 'Not Found')
Response.status(500, 'Internal Server Error')

// 设置单个标头
Response.header(name: string, value: string): Response

Response.header('Content-Type', 'application/json')
Response.header('X-Custom-Header', 'value')

// 设置多个标头
Response.headers(headers: Record<string, string>): Response

Response.headers({
  'Content-Type': 'application/json',
  'Cache-Control': 'no-cache',
  'X-API-Version': '1.0'
})

// 设置内容类型
Response.type(contentType: string): Response

Response.type('json')           // application/json
Response.type('html')           // text/html
Response.type('text/plain')     // text/plain
Response.type('png')            // image/png

// 设置 Vary 头
Response.vary(...fields: string[]): Response

Response.vary('Accept-Encoding', 'Accept-Language')

Cookies ​

typescript
// 设置单个 Cookie
Response.cookie(
  name: string,
  value: Value | null,
  options?: CookieOptions
): Response

Response.cookie('sessionId', 'abc123', {
  httpOnly: true,
  secure: true,
  maxAge: 86400000,  // 24 小时
  sameSite: 'lax'
})

// 删除 Cookie
Response.cookie('sessionId', null, {
  maxAge: 0
})

// 设置多个 Cookies
Response.cookies(
  cookies: Record<string, Value | null>,
  options?: CookieOptions
): Response

Response.cookies({
  sessionId: 'abc123',
  theme: 'dark',
  language: 'zh-CN'
}, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production'
})

type CookieOptions = {
  domain?: string                           // Cookie 域
  encode?: (val: string) => string          // 编码函数
  expires?: Date                            // 过期时间
  httpOnly?: boolean                        // 仅 HTTP 访问
  maxAge?: number                           // 最大生存时间(毫秒)
  path?: string                             // Cookie 路径
  priority?: 'low' | 'medium' | 'high'      // 优先级
  sameSite?: boolean | 'lax' | 'strict' | 'none'  // SameSite 策略
  secure?: boolean                          // 需要 HTTPS
  signed?: boolean                          // 签名 cookie
}

附件 ​

typescript
// 设置附件(下载)
Response.attachment(filename?: string, options?: AttachmentOptions): Response

Response.file('./document.pdf')
  .attachment('monthly-report.pdf')

// 中文文件名带后备
Response.file('./数据报告.xlsx')
  .attachment('数据报告.xlsx', {
    fallback: 'data-report.xlsx'
  })

// 内联显示
Response.file('./document.pdf')
  .attachment('document.pdf', {
    type: 'inline'
  })

type AttachmentOptions = {
  type?: 'attachment' | 'inline'  // 附件类型
  fallback?: string               // 后备文件名
}

响应操作 ​

类型检查 ​

typescript
// 检查响应内容类型
Response.is(...types: string[]): string | false

const response = Response.json({ data: 'test' })
response.is('json')        // 返回 'json'
response.is('html')        // 返回 false
response.is('json', 'xml') // 返回 'json'(第一个匹配)

const htmlResponse = Response.html('<h1>Hello</h1>')
htmlResponse.is('html')    // 返回 'html'
htmlResponse.is('text', 'html') // 返回 'html'

响应合并 ​

typescript
// 合并多个响应
Response.merge(...responses: Response[]): Response

// ✅ 正确:空主体 + JSON 主体 = JSON 主体
const baseResponse = Response.headers({
  'X-API-Version': 'v1',
  'X-Request-ID': requestId
})  // 保持空主体

const dataResponse = Response.json({ users: [] })
return baseResponse.merge(dataResponse)
// 结果:有 JSON 主体和所有标头

// ⚠️ 错误顺序:JSON 主体被空主体覆盖
const result = Response.json({ users: [] })
  .merge(Response.header('X-Version', 'v1'))
// 结果:只有空主体和标头,JSON 数据丢失!

// ✅ 正确顺序:空主体被 JSON 主体覆盖
const result = Response.header('X-Version', 'v1')
  .merge(Response.json({ users: [] }))
// 结果:有 JSON 主体和标头

// ✅ 最佳实践:使用链式调用
const result = Response.json({ users: [] })
  .header('X-Version', 'v1')
// 结果:有 JSON 主体和标头,无需担心顺序

重要规则:

  • 响应合并遵循后者覆盖前者原则
  • body 字段会被完全覆盖(不是合并)
  • status、headers、cookies 会被合并(后者优先)
  • vary 数组会被追加

matchBodyType - 响应拦截中间件 ​

函数签名:

typescript
function matchBodyType<T extends keyof BodyMap>(
  type: T,
  f: (body: BodyMap[T]) => MaybeAsyncResponse
): Middleware<any, MaybeAsyncResponse>

说明: 创建中间件来拦截和处理特定响应主体类型。

示例:

typescript
import { matchBodyType } from 'farrow-http'

// 为所有 JSON 响应添加时间戳
app.use(matchBodyType('json', (body) => {
  return Response.json({
    ...body.value,
    timestamp: Date.now(),
    version: 'v1'
  })
}))

// 为所有文件响应添加缓存标头
app.use(matchBodyType('file', (body) => {
  return Response.file(body.value, body.options)
    .header('Cache-Control', 'public, max-age=3600')
    .header('X-File-Server', 'farrow-http')
}))

// 处理字符串响应
app.use(matchBodyType('string', (body) => {
  return Response.string(`[${new Date().toISOString()}] ${body.value}`)
}))

// 所有可拦截类型
type BodyMap = {
  empty: EmptyBody
  string: StringBody
  json: JsonBody
  stream: StreamBody
  buffer: BufferBody
  file: FileBody
  redirect: RedirectBody
  custom: CustomBody
}

中间件系统 ​

中间件执行模型 ​

farrow-http 使用洋葱模型,每个中间件可以在请求处理前后执行代码:

typescript
app.use((req, next) => {
  console.log('1: 进入')       // 1
  const result = next(req)
  console.log('4: 退出')       // 4
  return result
})

app.use((req, next) => {
  console.log('2: 进入')       // 2
  const result = next(req)
  console.log('3: 退出')       // 3
  return result
})

// 执行顺序:1 → 2 → 3 → 4

核心原则:

  • ✅ 中间件必须返回 Response 对象
  • ✅ 调用 next(req) 继续到后续中间件
  • ✅ 不调用 next() 会中断执行链
  • ✅ 支持同步和异步中间件混合使用

中间件类型 ​

类型定义:

typescript
type HttpMiddleware = Middleware<RequestInfo, MaybeAsyncResponse>

type Middleware<I, O> = (input: I, next: Next<I, O>) => O

type Next<I, O> = (input?: I) => O

type MaybeAsyncResponse = Response | Promise<Response>

中间件编写模式 ​

1. 转换中间件 ​

修改输入传递给下一个中间件。

typescript
app.use((req, next) => {
  const transformed = {
    ...req,
    pathname: req.pathname.toLowerCase()
  }
  return next(transformed)
})

2. 增强中间件 ​

处理响应结果。

typescript
app.use((req, next) => {
  const response = next(req)
  return response.header('X-Powered-By', 'farrow-http')
})

3. 拦截中间件 ​

条件执行。

typescript
app.use((req, next) => {
  if (!validateRequest(req)) {
    return Response.status(400).text('Bad Request')
  }
  return next(req)
})

4. 包装中间件 ​

前后处理。

typescript
app.use((req, next) => {
  console.log(`开始处理: ${req.pathname}`)
  const start = Date.now()

  const response = next(req)

  console.log(`完成处理: ${Date.now() - start}ms`)
  return response
})

5. 终止中间件 ​

不调用 next,直接返回响应。

typescript
app.use((req) => {
  return Response.json({ message: 'Final result' })
})

全局中间件 ​

typescript
// 日志中间件
app.use(async (req, next) => {
  console.log(`${req.method} ${req.pathname}`)
  const start = Date.now()
  const response = await next(req)
  console.log(`耗时 ${Date.now() - start}ms`)
  return response
})

// CORS 中间件
app.use((req, next) => {
  const response = next(req)
  return response.headers({
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization'
  })
})

// 认证中间件
app.use(async (req, next) => {
  if (req.pathname.startsWith('/protected')) {
    const token = req.headers?.authorization
    if (!token) {
      throw new HttpError('需要授权', 401)
    }
    const user = await verifyToken(token)
    UserContext.set(user)
  }
  return next(req)
})

路由特定中间件 ​

typescript
app.get('/protected')
  .use(async (req, next) => {
    const token = req.headers.authorization
    if (!token) {
      return Response.status(401).json({ error: '未授权' })
    }
    return next(req)
  })
  .use((req) => {
    return Response.json({ message: '受保护的内容' })
  })

异步中间件 ​

typescript
app.use(async (req, next) => {
  const user = await authenticateUser(req)
  UserContext.set(user)
  return next(req)
})

// 处理异步响应
app.use(async (req, next) => {
  const response = await next(req)
  return response.header('X-Processed-At', Date.now().toString())
})

嵌套路由器中的中间件执行顺序 ​

typescript
// 父路由器的中间件会被子路由器继承
const apiRouter = app.route('/api')
apiRouter.use(corsMiddleware)     // 所有 API 路由都有 CORS
apiRouter.use(rateLimitMiddleware) // 所有 API 路由都有限流

const v1Router = apiRouter.route('/v1')
v1Router.use(authMiddleware)      // v1 路由需要认证

const userRouter = v1Router.route('/users')
userRouter.use(userValidationMiddleware)     // 用户路由有额外验证

// 请求 /api/v1/users 会依次经过:
// 1. corsMiddleware
// 2. rateLimitMiddleware
// 3. authMiddleware
// 4. userValidationMiddleware
// 5. 最终处理函数

上下文管理 ​

Context 上下文系统 ​

核心特性:

  • ✅ 请求级隔离 - 每个 HTTP 请求有独立的 Context 容器
  • ✅ 异步安全 - 基于 AsyncLocalStorage,在 async/await 中自动传递
  • ✅ 类型安全 - 完整的 TypeScript 类型推导
  • ✅ 自动启用 - farrow-http 自动启用异步追踪

创建上下文:

typescript
import { createContext } from 'farrow-pipeline'

// 创建上下文
const UserContext = createContext<User | null>(null)
const RequestIdContext = createContext<string>('')
const DBContext = createContext<Database>(defaultDB)

type Context<T> = {
  id: symbol                              // 唯一标识
  get(): T                                // 获取当前值
  set(value: T): void                     // 设置当前值
  assert(): Exclude<T, undefined | null>  // 断言非空
  create(value: T): Context<T>            // 创建新实例
}

使用上下文:

typescript
// 设置上下文
app.use((req, next) => {
  const user = authenticate(req)
  UserContext.set(user)

  const requestId = generateRequestId()
  RequestIdContext.set(requestId)

  return next(req)
})

// 读取上下文
app.use((req) => {
  const user = UserContext.get()          // User | null
  const requestId = RequestIdContext.get() // string

  if (!user) {
    return Response.status(401).json({ error: '未授权' })
  }

  return Response.json({ user, requestId })
})

// 断言非空
app.use((req) => {
  try {
    const user = UserContext.assert()  // User(不包含 null)
    return Response.json({ userId: user.id })
  } catch {
    return Response.status(401).json({ error: '未认证' })
  }
})

上下文隔离示例:

typescript
const CounterContext = createContext(0)

app.use((req, next) => {
  const count = CounterContext.get()
  CounterContext.set(count + 1)
  return next(req)
})

app.use((req) => {
  const count = CounterContext.get()
  return Response.json({ count })
})

// 并发请求,每个都有独立的计数器
await Promise.all([
  fetch('/'),  // count: 1
  fetch('/'),  // count: 1
  fetch('/')   // count: 1
])
// 每次运行都从默认值 0 开始

多上下文示例:

typescript
const UserContext = createContext<User | null>(null)
const LoggerContext = createContext<Logger>(consoleLogger)
const DBContext = createContext<Database>(defaultDB)

app.use((req, next) => {
  // 设置多个上下文
  UserContext.set(authenticate(req))
  LoggerContext.set(createRequestLogger(req))
  DBContext.set(getDatabaseConnection())

  return next(req)
})

app.use((req) => {
  // 使用多个上下文
  const user = UserContext.get()
  const logger = LoggerContext.get()
  const db = DBContext.get()

  logger.log(`User ${user?.id} accessing resource`)
  const data = db.query('SELECT * FROM resources')

  return Response.json(data)
})

上下文钩子 ​

useRequestInfo() ​

获取当前请求信息。

typescript
import { useRequestInfo } from 'farrow-http'

app.use(() => {
  const req = useRequestInfo()
  console.log({
    pathname: req.pathname,
    method: req.method,
    query: req.query,
    params: req.params,
    headers: req.headers,
    cookies: req.cookies
  })

  return Response.json({ ok: true })
})

useBasenames() ​

获取当前路由的基础路径列表。

typescript
import { useBasenames } from 'farrow-http'

const app = Http({ basenames: ['/api'] })
const v1Router = app.route('/v1')

v1Router.use(() => {
  const basenames = useBasenames() // ['/api', '/v1']
  return Response.json({ basenames })
})

usePrefix() ​

获取完整的路径前缀(拼接所有 basenames)。

typescript
import { usePrefix } from 'farrow-http'

app.route('/api').route('/v1').get('/status').use(() => {
  const prefix = usePrefix()  // '/api/v1'
  return Response.json({
    prefix,
    endpoint: `${prefix}/status`
  })
})

useRequest() 和 useReq() ​

获取 Node.js 原始请求对象。

typescript
import { useRequest, useReq } from 'farrow-http'

// useRequest() - 可能为 null
app.use(() => {
  const req = useRequest()  // IncomingMessage | null
  if (req) {
    console.log(req.method, req.url)
  }
  return Response.json({ ok: true })
})

// useReq() - 断言非空,如果为空则抛出错误
app.use(() => {
  const req = useReq()  // IncomingMessage(保证存在)
  console.log(req.headers)
  return Response.json({ ok: true })
})

useResponse() 和 useRes() ​

获取 Node.js 原始响应对象。

typescript
import { useResponse, useRes } from 'farrow-http'

// useResponse() - 可能为 null
app.use(() => {
  const res = useResponse()  // ServerResponse | null
  if (res) {
    // 直接操作(不推荐)
  }
  return Response.json({ ok: true })
})

// useRes() - 断言非空
app.use(() => {
  const res = useRes()  // ServerResponse(保证存在)
  // 保证存在
  return Response.json({ ok: true })
})

错误处理 ​

HttpError 类 ​

类型定义:

typescript
class HttpError extends Error {
  constructor(message: string, public statusCode: number = 500)
}

使用示例:

typescript
import { HttpError } from 'farrow-http'

// 抛出 HTTP 错误
app.use((req, next) => {
  if (!isValidRequest(req)) {
    throw new HttpError('错误请求', 400)
  }
  return next(req)
})

// 自定义错误类
class AuthenticationError extends HttpError {
  constructor(message = '需要认证') {
    super(message, 401)
  }
}

class ValidationError extends HttpError {
  constructor(message: string, public field?: string) {
    super(message, 400)
    this.name = 'ValidationError'
  }
}

// 使用自定义错误
app.use((req, next) => {
  if (!req.headers.authorization) {
    throw new AuthenticationError()
  }

  if (!validateInput(req.body)) {
    throw new ValidationError('Invalid email', 'email')
  }

  return next(req)
})

全局错误处理 ​

typescript
// 全局错误处理器
app.use(async (req, next) => {
  try {
    return await next(req)
  } catch (error) {
    console.error('请求失败:', {
      method: req.method,
      url: req.pathname,
      error: error.message
    })

    if (error instanceof ValidationError) {
      return Response.status(error.statusCode).json({
        error: error.message,
        field: error.field,
        type: 'validation_error'
      })
    }

    if (error instanceof HttpError) {
      return Response.status(error.statusCode).json({
        error: error.message,
        type: 'http_error'
      })
    }

    return Response.status(500).json({
      error: '内部服务器错误',
      type: 'server_error'
    })
  }
})

自动错误处理 ​

框架自动捕获中间件中的错误:

typescript
// 同步错误
app.use(() => {
  throw new Error('出了问题')
  // 自动转换为 500 响应
})

// 异步错误
app.use(async () => {
  const data = await fetchExternalAPI()
  return Response.json(data)
  // Promise 异常会被自动捕获
})

验证错误处理 ​

typescript
app.post('/users', {
  body: CreateUserInput
}, {
  onSchemaError: (error, input, next) => {
    // error.message: 验证错误消息
    // error.path: 字段路径,如 ['body', 'email']
    // error.value: 无效的值

    return Response.status(400).json({
      error: '验证失败',
      field: error.path?.join('.'),
      message: error.message,
      received: error.value
    })
  }
}).use((req) => {
  return Response.status(201).json(createUser(req.body))
})

错误堆栈控制 ​

typescript
const app = Http({
  // 开发环境显示完整堆栈
  errorStack: process.env.NODE_ENV === 'development'
})

工具函数 ​

ParseUrl - URL 模式类型解析 ​

类型定义:

typescript
type ParseUrl<T extends string> = {
  pathname: string
  params: ParseData<ParsePathname<T>>
  query: ParseData<ParseQueryString<T>>
}

说明: 从 URL 模式字符串中提取 TypeScript 类型。

示例:

typescript
import { ParseUrl } from 'farrow-http'

type T1 = ParseUrl<'/user/<id:int>'>
// {
//   pathname: string
//   params: { id: number }
//   query: {}
// }

type T2 = ParseUrl<'/posts?<sort:asc|desc>&<page?:int>'>
// {
//   pathname: string
//   params: {}
//   query: { sort: 'asc' | 'desc', page?: number }
// }

type T3 = ParseUrl<'/<category:string>/<id:int>?<expand?:string>'>
// {
//   pathname: string
//   params: { category: string, id: number }
//   query: { expand?: string }
// }

MarkReadOnlyDeep - 深度只读类型 ​

类型定义:

typescript
type MarkReadOnlyDeep<T> = T extends {} | any[]
  ? { readonly [key in keyof T]: MarkReadOnlyDeep<T[key]> }
  : T

说明: 递归地将对象的所有属性标记为只读。


最佳实践 ​

依赖注入模式 ​

使用 Context 实现依赖注入,便于测试和解耦。

typescript
// services/database.ts
import { createContext } from 'farrow-pipeline'

export interface Database {
  query: (sql: string) => Promise<any>
}

export const DatabaseContext = createContext<Database>(null as any)

// app.ts
import { createDatabaseConnection } from './db'

const app = Http({
  contexts: ({ req }) => {
    // 为每个请求注入依赖
    return {
      db: DatabaseContext.create(createDatabaseConnection())
    }
  }
})

// routes/users.ts
export const userRouter = Router()

userRouter.get('/<id:int>').use(async (req) => {
  const db = DatabaseContext.assert()
  const user = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`)
  return Response.json(user)
})

测试时注入 Mock:

typescript
import { createContainer } from 'farrow-pipeline'

test('GET /users/:id', async () => {
  const mockDB = {
    query: jest.fn().mockResolvedValue({ id: 1, name: 'Test' })
  }

  const testContainer = createContainer({
    db: DatabaseContext.create(mockDB)
  })

  const app = Http()
  app.route('/users').use(userRouter)

  const result = await app.run(
    { pathname: '/users/1', method: 'GET' },
    { container: testContainer }
  )

  expect(mockDB.query).toHaveBeenCalled()
})

测试策略 ​

单元测试(使用 supertest) ​

typescript
import { Http } from 'farrow-http'
import request from 'supertest'

const app = Http()
app.get('/health').use(() => Response.json({ status: 'ok' }))

const server = app.server()

test('health check', async () => {
  await request(server)
    .get('/health')
    .expect(200)
    .expect({ status: 'ok' })
})

集成测试(注入依赖) ​

typescript
test('create user with mock database', async () => {
  const mockDB = { save: jest.fn() }
  const container = createContainer({
    db: DatabaseContext.create(mockDB)
  })

  // Router() 返回 RouterPipeline,继承自 AsyncPipeline,有 run 方法
  const response = await userRouter.run(
    {
      pathname: '/users',
      method: 'POST',
      body: { name: 'Alice' }
    },
    { container }
  )

  expect(mockDB.save).toHaveBeenCalledWith({ name: 'Alice' })
})

总结 ​

farrow-http 是一个类型驱动的 Web 框架,核心优势:

  • 类型安全 - 编译期和运行时双重类型保障
  • 自动验证 - URL 模式自动推导类型并验证
  • 函数式 - 基于 farrow-pipeline 的纯函数中间件
  • 上下文隔离 - 基于 AsyncLocalStorage 的请求级隔离
  • 易于测试 - 清晰的接口和依赖注入
  • 高性能 - 轻量级实现,零运行时开销

通过这些特性,farrow-http 能够帮助你构建类型安全、可维护、高性能的 Web 应用程序。


相关文档:

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