farrow-http Complete API Reference
Table of Contents
- Core Concepts
- Complete Export List
- HTTP Server
- Router System
- Request & Validation
- Response Building
- Middleware System
- Context Management
- Error Handling
- Utilities
- Type Definitions
- Best Practices
Core Concepts
Design Philosophy
farrow-http is a type-driven web framework with core design principles:
- Type Safety First - Compile-time type checking with runtime automatic validation
- Declarative Routing - URL patterns automatically infer TypeScript types
- Functional Middleware - Onion model based on farrow-pipeline
- Automatic Validation - Integrated runtime validation with farrow-schema
- 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 buildingExecution Model
// 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:
import { Http } from 'farrow-http'
// Framework internally calls asyncTracerImpl.enable()
const app = Http()
// ✅ Context automatically propagates in async operationsComplete Export List
Main Entry (farrow-http)
// 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
// 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
// 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
// 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:
// 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:
npm install farrow-http farrow-pipeline farrow-schemaHTTP Server
Http - Create HTTP Server
Function Signature:
function Http(options?: HttpPipelineOptions): HttpPipelineType Definitions:
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:
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:
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.
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).
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).
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:
function Https(options?: HttpsPipelineOptions): HttpsPipelineType Definitions:
type HttpsPipelineOptions = HttpPipelineOptions & {
tls?: SecureContextOptions & TlsOptions
}
type HttpsPipeline = HttpPipeline // Same interfaceExample:
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:
function Router(): RouterPipelineType Definitions:
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:
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:
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?)
// 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?)
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?)
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?)
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?)
app.delete('/users/<id:int>').use((req) => {
deleteUser(req.params.id)
return Response.status(204).empty()
})head(path, schema?, options?)
app.head('/users').use(() => {
return Response.status(200).empty()
})options(path, schema?, options?)
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
// 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
// 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
// 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
// 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
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:
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:
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:
// 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:
route(name: string): Pipeline<RequestInfo, MaybeAsyncResponse>Description:
- Adds a path prefix (basename), returns a
Pipeline, not aRouterPipeline - The returned Pipeline does not have HTTP methods like
get,post - Used to organize routing hierarchy and mount independent
Router
Basic Usage:
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:
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):
// 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:
| Method | Return Type | Has HTTP Methods? | Use Case |
|---|---|---|---|
Router() | RouterPipeline | ✅ Yes | Create independent router |
app.route() | Pipeline | ❌ No | Add path prefix |
serve - Static File Service
Function Signature:
serve(name: string, dirname: string): voidDescription: 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:
// 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:
capture<T extends keyof BodyMap>(
type: T,
f: (body: BodyMap[T]) => MaybeAsyncResponse
): voidDescription: Intercept and transform responses of specific types.
Example:
// 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:
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:
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
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
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
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
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
// 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:
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
// 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
Response.text('Plain text response')
Response.status(404).text('Not Found')HTML Response
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
// 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
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
// 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
Response.status(204).empty()
Response.status(304).empty() // Not ModifiedBuffer Response
const buffer = Buffer.from('binary data')
Response.buffer(buffer)
.header('Content-Type', 'application/octet-stream')Custom Response
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
// 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
// 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
// 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
// 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
// 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 orderImportant Rules:
- Response merging follows latter overwrites former principle
bodyfield is completely overwritten (not merged)status,headers,cookiesare merged (latter takes precedence)varyarrays are concatenated
matchBodyType - Response Interception Middleware
Function Signature:
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:
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:
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 → 4Core 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:
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.
app.use((req, next) => {
const transformed = {
...req,
pathname: req.pathname.toLowerCase()
}
return next(transformed)
})2. Enhance Middleware
Process response result.
app.use((req, next) => {
const response = next(req)
return response.header('X-Powered-By', 'farrow-http')
})3. Intercept Middleware
Conditional execution.
app.use((req, next) => {
if (!validateRequest(req)) {
return Response.status(400).text('Bad Request')
}
return next(req)
})4. Wrap Middleware
Before and after processing.
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.
app.use((req) => {
return Response.json({ message: 'Final result' })
})Global Middleware
// 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
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
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
// 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 handlerContext 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:
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:
// 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:
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 0Multiple Contexts Example:
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.
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.
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).
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.
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.
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:
class HttpError extends Error {
constructor(message: string, public statusCode: number = 500)
}Usage Example:
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
// 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:
// 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
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
const app = Http({
// Show full stack in development
errorStack: process.env.NODE_ENV === 'development'
})Utilities
ParseUrl - URL Pattern Type Parsing
Type Definition:
type ParseUrl<T extends string> = {
pathname: string
params: ParseData<ParsePathname<T>>
query: ParseData<ParseQueryString<T>>
}Description: Extract TypeScript types from URL pattern strings.
Example:
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:
type MarkReadOnlyDeep<T> = T extends {} | any[]
? { readonly [key in keyof T]: MarkReadOnlyDeep<T[key]> }
: TDescription: 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.
// 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:
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)
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)
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:
