Skip to content

farrow-http Complete API Reference

Table of Contents

  1. Core Concepts
  2. Complete Export List
  3. HTTP Server
  4. Router System
  5. Request & Validation
  6. Response Building
  7. Middleware System
  8. Context Management
  9. Error Handling
  10. Utilities
  11. Type Definitions
  12. Best Practices

Core Concepts

Design Philosophy

farrow-http is a type-driven web framework with core design principles:

  1. Type Safety First - Compile-time type checking with runtime automatic validation
  2. Declarative Routing - URL patterns automatically infer TypeScript types
  3. Functional Middleware - Onion model based on farrow-pipeline
  4. Automatic Validation - Integrated runtime validation with farrow-schema
  5. Context Isolation - Request-level isolation based on AsyncLocalStorage

Three Pillars

farrow-pipeline (middleware pipeline)  →  Process flow control
farrow-schema   (type validation)      →  Automatic validation and type inference
farrow-http     (HTTP framework)       →  Routing and response building

Execution Model

typescript
// Onion model execution order
app.use((req, next) => {
  console.log('1: Enter')       // 1
  const res = next(req)
  console.log('4: Exit')        // 4
  return res
})

app.use((req, next) => {
  console.log('2: Enter')       // 2
  const res = next(req)
  console.log('3: Exit')        // 3
  return res
})

Automatic Async Tracing

farrow-http automatically enables async tracing on startup without manual configuration:

typescript
import { Http } from 'farrow-http'

// Framework internally calls asyncTracerImpl.enable()
const app = Http()
// ✅ Context automatically propagates in async operations

Complete Export List

Main Entry (farrow-http)

typescript
// HTTP Server
export { Http, Https }
export type { HttpPipeline, HttpsPipeline, HttpPipelineOptions, HttpsPipelineOptions }

// Router System
export { Router }
export type {
  RouterPipeline,
  RoutingMethods,
  RoutingMethod,
  RouterUrlSchema,
  RouterRequestSchema,
  RouterSharedSchema,
  MatchOptions,
  HttpMiddleware,
  HttpMiddlewareInput,
  Pathname
}

// Request Info
export type {
  RequestInfo,
  RequestQuery,
  RequestHeaders,
  RequestCookies
}

// Response Building
export { Response, matchBodyType }
export type { MaybeAsyncResponse }

// Response Info
export type {
  ResponseInfo,
  Body,
  BodyMap,
  Status,
  Headers,
  Cookies,
  EmptyBody,
  StringBody,
  JsonBody,
  StreamBody,
  BufferBody,
  FileBody,
  RedirectBody,
  CustomBody,
  FileBodyOptions,
  CustomBodyHandler,
  RedirectOptions
}

// Context Hooks
export {
  useReq,           // Get Node.js request object (asserted non-null)
  useRequest,       // Get Node.js request object (nullable)
  useRequestInfo,   // Get farrow request info
  useRes,           // Get Node.js response object (asserted non-null)
  useResponse       // Get Node.js response object (nullable)
}

// Basename Routing
export { useBasenames, usePrefix }

// Error Handling
export { HttpError }

// Logger
export type { LoggerOptions, LoggerEvent }

// Type Utilities
export type { ParseUrl, MarkReadOnlyDeep }

Companion Dependency Modules

farrow-http depends on the following modules, which need to be installed and imported separately:

farrow-pipeline - Middleware Pipeline System

typescript
// Import from 'farrow-pipeline' (not from 'farrow-http')
import {
  createContext,        // Create context
  createContainer,      // Create container
  useContainer,         // Get current container
  runWithContainer,     // Run in specified container
  type Context,         // Context type
  type Container,       // Container type
  type ContextStorage,  // Context storage type
  type Middleware,      // Middleware type
  type Pipeline,        // Sync pipeline type
  type AsyncPipeline,   // Async pipeline type
  type Next,            // Next function type
  type MaybeAsync       // Maybe async type
} from 'farrow-pipeline'

farrow-schema - Type Definition and Validation

typescript
// Import from 'farrow-schema' (not from 'farrow-http')
import {
  Schema,               // Schema base class
  ObjectType,           // Object type (supports recursion)
  StructType,           // Struct type (no recursion)

  // Basic types
  String, Number, Boolean, Int, Float, ID, Date,

  // Composite type constructors
  List, Optional, Nullable, Record, Tuple,

  // Union and intersection
  Union, Intersect, Literal,

  // Struct constructor
  Struct,

  // Special types
  Any, Unknown, Never, Json,

  // Utilities
  TypeOf,               // Extract TypeScript type
  field,                // Define field metadata

  // Type definitions
  type FieldDescriptor,
  type FieldDescriptors,
  type SchemaCtor
} from 'farrow-schema'

farrow-schema/validator - Runtime Validation

typescript
// Import from 'farrow-schema/validator' (not from 'farrow-http')
import {
  Validator,            // Validator object
  createSchemaValidator,// Create dedicated validator
  ValidatorType,        // Custom validator base class
  RegExp,               // RegExp validator

  // Type definitions
  type ValidationResult,
  type ValidationError,
  type ValidatorOptions
} from 'farrow-schema/validator'

Complete Usage Example:

typescript
// 1. Import modules separately
import { Http, Response } from 'farrow-http'
import { createContext } from 'farrow-pipeline'
import { ObjectType, String, Int, Optional } from 'farrow-schema'

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

// 3. Create context
const UserContext = createContext<User | null>(null)

// 4. Create HTTP server
const app = Http()

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

app.listen(3000)

Install Dependencies:

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

HTTP Server

Http - Create HTTP Server

Function Signature:

typescript
function Http(options?: HttpPipelineOptions): HttpPipeline

Type Definitions:

typescript
type HttpPipelineOptions = {
  basenames?: string[]                    // Base path prefixes
  body?: BodyOptions                      // Request body parsing options (from co-body)
  cookie?: CookieParseOptions             // Cookie parsing options (from cookie)
  query?: IParseOptions                   // Query param parsing options (from qs)
  contexts?: (params: {                   // Context injection function
    req: IncomingMessage
    requestInfo: RequestInfo
    basename: string
  }) => ContextStorage
  logger?: boolean | HttpLoggerOptions    // Logger configuration
  errorStack?: boolean                    // Show error stack
}

type HttpLoggerOptions = {
  transporter?: (str: string) => void                        // Custom log output
  ignoreIntrospectionRequestOfFarrowApi?: boolean           // Ignore farrow-api introspection (default true)
}

// BodyOptions - from co-body library
// Used to parse HTTP request body (JSON, form, text)
// co-body is the underlying dependency of koa-bodyparser
type BodyOptions = {
  limit?: string | number          // Request body size limit, e.g. '1mb', '10kb', 1024000
  encoding?: BufferEncoding        // Character encoding, default 'utf8'
  strict?: boolean                 // JSON strict mode, default true
  jsonTypes?: string[]             // Custom JSON content-type list, default ['application/json']
  formTypes?: string[]             // Custom form content-type list, default ['application/x-www-form-urlencoded']
  textTypes?: string[]             // Custom text content-type list, default ['text/plain']
}

// CookieParseOptions - from cookie library
// Used to parse HTTP Cookie header
type CookieParseOptions = {
  decode?: (val: string) => string  // Cookie value decode function, default decodeURIComponent
}

// IParseOptions - from qs library
// Used to parse URL query string (e.g. ?a=1&b=2)
type IParseOptions = {
  depth?: number                   // Object nesting depth limit, default 5
  arrayLimit?: number              // Array element count limit, default 20
  delimiter?: string | RegExp      // Query param delimiter, default '&'
  allowDots?: boolean              // Allow dot notation, default false (e.g. ?user.name=Alice)
  parseArrays?: boolean            // Parse array syntax, default true (e.g. ?tags[0]=a&tags[1]=b)
  allowPrototypes?: boolean        // Allow prototype pollution, default false (security)
  plainObjects?: boolean           // Use plain objects (no prototype), default false
  comma?: boolean                  // Treat comma as array delimiter, default false
}

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

Basic Example:

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

// Create server
const app = Http()

// Add route
app.get('/').use(() => Response.json({ message: 'Hello World' }))

// Start server
app.listen(3000, () => console.log('Server running at http://localhost:3000'))

Configuration Example:

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

Server Methods:

listen(port, callback?)

Start the server and listen on a port.

typescript
app.listen(3000)
app.listen(3000, () => console.log('Server started'))
app.listen(3000, '0.0.0.0', () => console.log('Listening on all interfaces'))

server()

Get the underlying Node.js HTTP server instance (for testing).

typescript
const server = app.server()  // http.Server instance

// For testing
import request from 'supertest'
const response = await request(server).get('/').expect(200)

handle(req, res, options?)

Directly handle Node.js native HTTP requests (for integration with other frameworks or custom servers).

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

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

// Method 1: Standalone (recommended)
app.listen(3000)

// Method 2: Integrate with Express
const expressApp = express()
expressApp.use('/farrow', (req, res) => {
  app.handle(req, res)
})
expressApp.listen(3000)

// Method 3: Custom server
const customServer = createServer(app.handle)
customServer.listen(3000)

Note: app.server() internally calls createServer(app.handle).


Https - Create HTTPS Server

Function Signature:

typescript
function Https(options?: HttpsPipelineOptions): HttpsPipeline

Type Definitions:

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

type HttpsPipeline = HttpPipeline  // Same interface

Example:

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 server running on port 443'))

Router System

Router - Create Independent Router

Function Signature:

typescript
function Router(): RouterPipeline

Type Definitions:

typescript
type RouterPipeline = AsyncPipeline<RequestInfo, Response> & {
  // HTTP method routing
  get: RoutingMethod
  post: RoutingMethod
  put: RoutingMethod
  patch: RoutingMethod
  delete: RoutingMethod
  head: RoutingMethod
  options: RoutingMethod

  // Advanced matching
  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>

  // Nested routing
  route(name: string): Pipeline<RequestInfo, MaybeAsyncResponse>

  // Static files
  serve(name: string, dirname: string): void

  // Response interception
  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>

Basic Example:

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 })
})

// Mount to app
app.route('/users').use(userRouter)

HTTP Method Routing

All HTTP methods support the same signature:

typescript
type RoutingMethod = <U extends string, T extends RouterSharedSchema>(
  path: U,           // URL pattern
  schema?: T,        // Optional validation Schema
  options?: MatchOptions  // Optional match options
) => Pipeline<...>

get(path, schema?, options?)

typescript
// Basic route
app.get('/').use(() => Response.text('Home'))

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

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

// With validated headers
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 is validated and type-safe
  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 Patterns and Automatic Validation

farrow-http provides a powerful URL pattern system with automatic type inference and validation.

Path Parameter Types

typescript
// Basic types
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 (non-empty)
})

Union Types

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

// Literal types
app.get('/api/<version:{v1}|{v2}>').use((req) => {
  req.params.version  // 'v1' | 'v2'
})

Modifiers

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

// One or more (+)
app.get('/tags/<tags+:string>').use((req) => {
  req.params.tags  // string[]
})

// Zero or more (*)
app.get('/categories/<cats*:string>').use((req) => {
  req.params.cats  // string[] | undefined
})

Query Parameters

typescript
// Required query param
app.get('/products?<sort:asc|desc>').use((req) => {
  req.query.sort  // 'asc' | 'desc'
})

// Optional query param
app.get('/search?<q:string>&<page?:int>').use((req) => {
  const { q, page = 1 } = req.query
  // q: string, page: number | undefined
})

// Array query param
app.get('/filter?<tags*:string>').use((req) => {
  req.query.tags  // string[] | undefined
})

// Literal query param
app.get('/products?status=active').use((req) => {
  req.query.status  // 'active'
})

Combined Example

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 - Advanced Route Matching

Function Signature:

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>

Type Definitions:

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                     // Block mode
  onSchemaError?(                     // Validation error handler
    error: ValidationError,
    input: RequestInfo,
    next: Next<RequestInfo, MaybeAsyncResponse>
  ): MaybeAsyncResponse | void
}

Usage Example:

typescript
// URL Schema approach
app.match({
  url: '/users/<id:int>?<expand?:string>',
  method: ['GET', 'PUT'],
  body: { name: String, email: String },
  headers: { authorization: String }
}, {
  block: true,  // Block mode: validation failure returns 400
  onSchemaError: (error, input, next) => {
    return Response.status(400).json({
      error: 'Validation failed',
      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 approach
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 - Path Prefix (Basename)

Function Signature:

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

Description:

  • Adds a path prefix (basename), returns a Pipeline, not a RouterPipeline
  • The returned Pipeline does not have HTTP methods like get, post
  • Used to organize routing hierarchy and mount independent Router

Basic Usage:

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

// ❌ Wrong: route() returns Pipeline without HTTP methods
const v1 = app.route('/v1')
v1.get('/users')  // Error! Pipeline doesn't have get method

// ✅ Correct: Create independent Router then mount
const v1Router = Router()
v1Router.get('/users').use(() => Response.json({ users: [] }))
v1Router.get('/posts').use(() => Response.json({ posts: [] }))

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

Nested Routing:

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

// Continue nesting route
app.route('/v1')
  .route('/users')
  .use((req) => {
    // Request /api/v1/users/* arrives here
    // req.pathname has /api/v1/users prefix removed
    return Response.json({ path: req.pathname })
  })

Modular Routing (Recommended):

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'] })

// Mount independent Routers
app.route('/users').use(userRouter)   // GET /api/users
app.route('/posts').use(postRouter)   // GET /api/posts

// Nested mounting
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)

Key Differences:

MethodReturn TypeHas HTTP Methods?Use Case
Router()RouterPipeline✅ YesCreate independent router
app.route()Pipeline❌ NoAdd path prefix

serve - Static File Service

Function Signature:

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

Description: Provides static file serving with built-in security protection.

Built-in Security Features:

  • ✅ Automatic prevention of directory traversal attacks (path normalization)
  • ✅ Automatic file permission checking
  • ✅ Provides index.html for directory requests
  • ✅ Gracefully passes to next middleware if file not found
  • ✅ Cross-platform safe path handling

Example:

typescript
// Basic static file service
app.serve('/static', './public')
app.serve('/uploads', './storage/uploads')

// URL mapping:
// /static/style.css        → ./public/style.css
// /static/                 → ./public/index.html (if exists)
// /static/docs/            → ./public/docs/index.html (if exists)
// /uploads/../secret       → Blocked (directory traversal protection)

// Combined usage
app.serve('/assets', './public')
app.get('/api/users').use(() => Response.json({ users: [] }))

capture - Response Interception

Function Signature:

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

Description: Intercept and transform responses of specific types.

Example:

typescript
// Add timestamp to all JSON responses
app.capture('json', (jsonBody) => {
  return Response.json({
    data: jsonBody.value,
    timestamp: new Date().toISOString(),
    version: 'v1'
  })
})

// Add cache headers to all file responses
app.capture('file', (fileBody) => {
  return Response.file(fileBody.value, fileBody.options)
    .header('Cache-Control', 'public, max-age=3600')
    .header('X-File-Server', 'farrow-http')
})

// Process string responses
app.capture('string', (stringBody) => {
  return Response.string(`[${new Date().toISOString()}] ${stringBody.value}`)
})

Capturable Response Types:

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

Request & Validation

RequestInfo - Request Info Type

Type Definition:

typescript
type RequestInfo = {
  readonly pathname: string         // Request path, e.g. "/users/123"
  readonly method?: string          // HTTP method, e.g. "GET"
  readonly query?: RequestQuery     // Query parameters
  readonly body?: any               // Request body (parsed and validated)
  readonly headers?: RequestHeaders // Request headers
  readonly cookies?: RequestCookies // Cookies
  readonly params?: RequestParams   // Path parameters (parsed from URL pattern)
}

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

Features:

  • ✅ All fields are readonly
  • ✅ Path parameters, query parameters, and request body are automatically validated and type-converted
  • ✅ Maintains consistency across entire middleware chain

Request Body Validation

Using 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 is validated and type-safe
  const { title, content, authorId, tags = [], published = false } = req.body

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

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

Using 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 })
})

Nested Object Validation

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 is validated as AddressInput
  const { name, email, address, contacts = [] } = req.body

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

Custom Validation Error Handling

typescript
app.post('/users', {
  body: CreateUserInput
}, {
  onSchemaError: (error, input, next) => {
    // error.message: error message
    // error.path: field path, e.g. ['body', 'email']
    // error.value: invalid value

    return Response.status(400).json({
      error: 'Data validation failed',
      field: error.path?.join('.'),  // 'body.email'
      message: error.message,
      received: error.value,
      hint: 'Please check input format'
    })
  }
}).use((req) => {
  return Response.status(201).json(createUser(req.body))
})

Headers and Cookies Validation

typescript
// Headers validation
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 validation
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 })
})

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

Response Building

Response - Response Object

Type Definition:

typescript
type Response = {
  info: ResponseInfo                    // Response info
  merge: (...responses: Response[]) => Response  // Merge responses
  is: (...types: string[]) => string | false     // Type check

  // Response body methods
  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

  // Response metadata methods
  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
}

Basic Response Types

JSON Response

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

// With status code
Response.status(201).json({ id: 1, name: 'Alice' })

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

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

Text Response

typescript
Response.text('Plain text response')
Response.status(404).text('Not Found')

HTML Response

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

Response.status(200).html(`
  <!DOCTYPE html>
  <html>
    <head><title>Welcome</title></head>
    <body><h1>Welcome to farrow-http</h1></body>
  </html>
`)

File Response

typescript
// Basic file response
Response.file('./uploads/document.pdf')

// With content type
Response.file('./images/logo.png', 'image/png')

// With stream control options
Response.file('./large-file.zip', {
  start: 0,
  end: 1024 * 1024 - 1,  // Read only first 1MB
  highWaterMark: 1024 * 1024  // 1MB buffer
})

// As download attachment
Response.file('./report.pdf')
  .attachment('monthly-report.pdf')
  .header('Cache-Control', 'private, max-age=3600')

// Chinese filename with fallback
Response.file('./数据报告.xlsx')
  .attachment('数据报告.xlsx', {
    fallback: 'data-report.xlsx'  // For legacy browsers
  })

// Inline display (open in browser instead of download)
Response.file('./document.pdf')
  .attachment('document.pdf', {
    type: 'inline'
  })

Stream Response

typescript
import { Readable } from 'stream'

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

// From file
import fs from 'fs'
const fileStream = fs.createReadStream('./large-file.log')
Response.stream(fileStream)
  .header('Content-Type', 'text/plain')

Redirect Response

typescript
// Basic redirect
Response.redirect('/login')
Response.redirect('https://example.com')

// Without route prefix
Response.redirect('/path', { usePrefix: false })

// Behavior in nested routes
const apiRouter = app.route('/api')
apiRouter.use(() => {
  return Response.redirect('/users')           // Redirects to /api/users
})
apiRouter.use(() => {
  return Response.redirect('/users', { usePrefix: false })  // Redirects to /users
})

// Redirect to referer
Response.redirect('back')  // Redirects to HTTP Referer or '/'

Empty Response

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

Buffer Response

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

Custom Response

typescript
Response.custom(({ req, res, requestInfo, responseInfo }) => {
  // req: IncomingMessage - Node.js request object
  // res: ServerResponse - Node.js response object
  // requestInfo: RequestInfo - farrow request info
  // responseInfo: Omit<ResponseInfo, 'body'> - farrow response info (no body)

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

// Server-Sent Events (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: 'Connected' })

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

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

Response Methods (Chainable)

Status and Headers

typescript
// Set status code
Response.status(code: number, message?: string): Response

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

// Set single header
Response.header(name: string, value: string): Response

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

// Set multiple headers
Response.headers(headers: Record<string, string>): Response

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

// Set content type
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

// Set Vary header
Response.vary(...fields: string[]): Response

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

Cookies

typescript
// Set single cookie
Response.cookie(
  name: string,
  value: Value | null,
  options?: CookieOptions
): Response

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

// Delete cookie
Response.cookie('sessionId', null, {
  maxAge: 0
})

// Set multiple 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 domain
  encode?: (val: string) => string          // Encode function
  expires?: Date                            // Expiration time
  httpOnly?: boolean                        // HTTP only access
  maxAge?: number                           // Max age (milliseconds)
  path?: string                             // Cookie path
  priority?: 'low' | 'medium' | 'high'      // Priority
  sameSite?: boolean | 'lax' | 'strict' | 'none'  // SameSite policy
  secure?: boolean                          // Requires HTTPS
  signed?: boolean                          // Signed cookie
}

Attachment

typescript
// Set attachment (download)
Response.attachment(filename?: string, options?: AttachmentOptions): Response

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

// Chinese filename with fallback
Response.file('./数据报告.xlsx')
  .attachment('数据报告.xlsx', {
    fallback: 'data-report.xlsx'
  })

// Inline display
Response.file('./document.pdf')
  .attachment('document.pdf', {
    type: 'inline'
  })

type AttachmentOptions = {
  type?: 'attachment' | 'inline'  // Attachment type
  fallback?: string               // Fallback filename
}

Response Operations

Type Check

typescript
// Check response content type
Response.is(...types: string[]): string | false

const response = Response.json({ data: 'test' })
response.is('json')        // Returns 'json'
response.is('html')        // Returns false
response.is('json', 'xml') // Returns 'json' (first match)

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

Response Merging

typescript
// Merge multiple responses
Response.merge(...responses: Response[]): Response

// ✅ Correct: empty body + JSON body = JSON body
const baseResponse = Response.headers({
  'X-API-Version': 'v1',
  'X-Request-ID': requestId
})  // Keeps empty body

const dataResponse = Response.json({ users: [] })
return baseResponse.merge(dataResponse)
// Result: Has JSON body and all headers

// ⚠️ Wrong order: JSON body overwritten by empty body
const result = Response.json({ users: [] })
  .merge(Response.header('X-Version', 'v1'))
// Result: Only empty body and headers, JSON data lost!

// ✅ Correct order: empty body overwritten by JSON body
const result = Response.header('X-Version', 'v1')
  .merge(Response.json({ users: [] }))
// Result: Has JSON body and headers

// ✅ Best practice: use method chaining
const result = Response.json({ users: [] })
  .header('X-Version', 'v1')
// Result: Has JSON body and headers, no need to worry about order

Important Rules:

  • Response merging follows latter overwrites former principle
  • body field is completely overwritten (not merged)
  • status, headers, cookies are merged (latter takes precedence)
  • vary arrays are concatenated

matchBodyType - Response Interception Middleware

Function Signature:

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

Description: Create middleware to intercept and process specific response body types.

Example:

typescript
import { matchBodyType } from 'farrow-http'

// Add timestamp to all JSON responses
app.use(matchBodyType('json', (body) => {
  return Response.json({
    ...body.value,
    timestamp: Date.now(),
    version: 'v1'
  })
}))

// Add cache headers to all file responses
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')
}))

// Process string responses
app.use(matchBodyType('string', (body) => {
  return Response.string(`[${new Date().toISOString()}] ${body.value}`)
}))

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

Middleware System

Middleware Execution Model

farrow-http uses the onion model, where each middleware can execute code before and after request processing:

typescript
app.use((req, next) => {
  console.log('1: Enter')       // 1
  const result = next(req)
  console.log('4: Exit')        // 4
  return result
})

app.use((req, next) => {
  console.log('2: Enter')       // 2
  const result = next(req)
  console.log('3: Exit')        // 3
  return result
})

// Execution order: 1 → 2 → 3 → 4

Core Principles:

  • ✅ Middleware must return Response object
  • ✅ Call next(req) to continue to subsequent middleware
  • ✅ Not calling next() breaks the execution chain
  • ✅ Supports mixing synchronous and asynchronous middleware

Middleware Types

Type Definition:

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>

Middleware Writing Patterns

1. Transform Middleware

Modify input passed to next middleware.

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

2. Enhance Middleware

Process response result.

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

3. Intercept Middleware

Conditional execution.

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

4. Wrap Middleware

Before and after processing.

typescript
app.use((req, next) => {
  console.log(`Start processing: ${req.pathname}`)
  const start = Date.now()

  const response = next(req)

  console.log(`Finished processing: ${Date.now() - start}ms`)
  return response
})

5. Terminal Middleware

Don't call next, return response directly.

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

Global Middleware

typescript
// Logging middleware
app.use(async (req, next) => {
  console.log(`${req.method} ${req.pathname}`)
  const start = Date.now()
  const response = await next(req)
  console.log(`Took ${Date.now() - start}ms`)
  return response
})

// CORS middleware
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'
  })
})

// Authentication middleware
app.use(async (req, next) => {
  if (req.pathname.startsWith('/protected')) {
    const token = req.headers?.authorization
    if (!token) {
      throw new HttpError('Authorization required', 401)
    }
    const user = await verifyToken(token)
    UserContext.set(user)
  }
  return next(req)
})

Route-Specific Middleware

typescript
app.get('/protected')
  .use(async (req, next) => {
    const token = req.headers.authorization
    if (!token) {
      return Response.status(401).json({ error: 'Unauthorized' })
    }
    return next(req)
  })
  .use((req) => {
    return Response.json({ message: 'Protected content' })
  })

Async Middleware

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

// Process async response
app.use(async (req, next) => {
  const response = await next(req)
  return response.header('X-Processed-At', Date.now().toString())
})

Middleware Execution Order in Nested Routers

typescript
// Parent router middleware is inherited by child routers
const apiRouter = app.route('/api')
apiRouter.use(corsMiddleware)     // All API routes have CORS
apiRouter.use(rateLimitMiddleware) // All API routes have rate limiting

const v1Router = apiRouter.route('/v1')
v1Router.use(authMiddleware)      // v1 routes require authentication

const userRouter = v1Router.route('/users')
userRouter.use(userValidationMiddleware)     // User routes have extra validation

// Request to /api/v1/users goes through sequentially:
// 1. corsMiddleware
// 2. rateLimitMiddleware
// 3. authMiddleware
// 4. userValidationMiddleware
// 5. Final handler

Context Management

Context System

Core Features:

  • Request-level isolation - Each HTTP request has independent Context container
  • Async-safe - Based on AsyncLocalStorage, automatically propagates in async/await
  • Type-safe - Complete TypeScript type inference
  • Auto-enabled - farrow-http automatically enables async tracing

Create Context:

typescript
import { createContext } from 'farrow-pipeline'

// Create contexts
const UserContext = createContext<User | null>(null)
const RequestIdContext = createContext<string>('')
const DBContext = createContext<Database>(defaultDB)

type Context<T> = {
  id: symbol                              // Unique identifier
  get(): T                                // Get current value
  set(value: T): void                     // Set current value
  assert(): Exclude<T, undefined | null>  // Assert non-null
  create(value: T): Context<T>            // Create new instance
}

Use Context:

typescript
// Set context
app.use((req, next) => {
  const user = authenticate(req)
  UserContext.set(user)

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

  return next(req)
})

// Read context
app.use((req) => {
  const user = UserContext.get()          // User | null
  const requestId = RequestIdContext.get() // string

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

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

// Assert non-null
app.use((req) => {
  try {
    const user = UserContext.assert()  // User (excludes null)
    return Response.json({ userId: user.id })
  } catch {
    return Response.status(401).json({ error: 'Unauthenticated' })
  }
})

Context Isolation Example:

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 })
})

// Concurrent requests, each with independent counter
await Promise.all([
  fetch('/'),  // count: 1
  fetch('/'),  // count: 1
  fetch('/')   // count: 1
])
// Each run starts from default value 0

Multiple Contexts Example:

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

app.use((req, next) => {
  // Set multiple contexts
  UserContext.set(authenticate(req))
  LoggerContext.set(createRequestLogger(req))
  DBContext.set(getDatabaseConnection())

  return next(req)
})

app.use((req) => {
  // Use multiple contexts
  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)
})

Context Hooks

useRequestInfo()

Get current request info.

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()

Get current route's basename list.

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()

Get complete path prefix (concatenated 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() and useReq()

Get Node.js native request object.

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

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

// useReq() - asserts non-null, throws if null
app.use(() => {
  const req = useReq()  // IncomingMessage (guaranteed to exist)
  console.log(req.headers)
  return Response.json({ ok: true })
})

useResponse() and useRes()

Get Node.js native response object.

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

// useResponse() - may be null
app.use(() => {
  const res = useResponse()  // ServerResponse | null
  if (res) {
    // Direct manipulation (not recommended)
  }
  return Response.json({ ok: true })
})

// useRes() - asserts non-null
app.use(() => {
  const res = useRes()  // ServerResponse (guaranteed to exist)
  // Guaranteed to exist
  return Response.json({ ok: true })
})

Error Handling

HttpError Class

Type Definition:

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

Usage Example:

typescript
import { HttpError } from 'farrow-http'

// Throw HTTP error
app.use((req, next) => {
  if (!isValidRequest(req)) {
    throw new HttpError('Bad Request', 400)
  }
  return next(req)
})

// Custom error classes
class AuthenticationError extends HttpError {
  constructor(message = 'Authentication required') {
    super(message, 401)
  }
}

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

// Use custom errors
app.use((req, next) => {
  if (!req.headers.authorization) {
    throw new AuthenticationError()
  }

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

  return next(req)
})

Global Error Handling

typescript
// Global error handler
app.use(async (req, next) => {
  try {
    return await next(req)
  } catch (error) {
    console.error('Request failed:', {
      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: 'Internal Server Error',
      type: 'server_error'
    })
  }
})

Automatic Error Handling

The framework automatically catches errors in middleware:

typescript
// Sync errors
app.use(() => {
  throw new Error('Something went wrong')
  // Automatically converted to 500 response
})

// Async errors
app.use(async () => {
  const data = await fetchExternalAPI()
  return Response.json(data)
  // Promise rejection is automatically caught
})

Validation Error Handling

typescript
app.post('/users', {
  body: CreateUserInput
}, {
  onSchemaError: (error, input, next) => {
    // error.message: validation error message
    // error.path: field path, e.g. ['body', 'email']
    // error.value: invalid value

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

Error Stack Control

typescript
const app = Http({
  // Show full stack in development
  errorStack: process.env.NODE_ENV === 'development'
})

Utilities

ParseUrl - URL Pattern Type Parsing

Type Definition:

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

Description: Extract TypeScript types from URL pattern strings.

Example:

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 - Deep Readonly Type

Type Definition:

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

Description: Recursively mark all properties of an object as readonly.


Best Practices

Dependency Injection Pattern

Use Context to implement dependency injection for easier testing and decoupling.

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 }) => {
    // Inject dependencies for each request
    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)
})

Inject Mock During Testing:

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()
})

Testing Strategies

Unit Testing (using 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' })
})

Integration Testing (inject dependencies)

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

  // Router() returns RouterPipeline, inherits from AsyncPipeline, has run method
  const response = await userRouter.run(
    {
      pathname: '/users',
      method: 'POST',
      body: { name: 'Alice' }
    },
    { container }
  )

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

Summary

farrow-http is a type-driven web framework with core advantages:

Type Safety - Dual type protection at compile-time and runtime ✅ Automatic Validation - URL patterns automatically infer types and validate ✅ Functional - Pure function middleware based on farrow-pipeline ✅ Context Isolation - Request-level isolation based on AsyncLocalStorage ✅ Easy Testing - Clear interfaces and dependency injection ✅ High Performance - Lightweight implementation, zero runtime overhead

Through these features, farrow-http helps you build type-safe, maintainable, high-performance web applications.


Related Documentation:

This is a third-party Farrow documentation site | Built with ❤️ and TypeScript