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 没有 getpost 等 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 字段会被完全覆盖(不是合并)
  • statusheaderscookies 会被合并(后者优先)
  • 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 构建