实战与进阶
完整实战项目:博客 API
让我们构建一个完整的博客 API,包含所有最佳实践:
typescript
import { Http, Router, Response } from 'farrow-http'
import { ObjectType, String, List, Union, Literal, Optional } from 'farrow-schema'
import { createContext } from 'farrow-pipeline'
// ========== 数据模型 ==========
class Article extends ObjectType {
id = String
title = String
content = String
authorId = String
status = Union(
Literal('draft'),
Literal('published'),
Literal('archived')
)
tags = List(String)
createdAt = String
}
class CreateArticleInput extends ObjectType {
title = String
content = String
tags = List(String)
status = Union(Literal('draft'), Literal('published'))
}
// ========== 上下文 ==========
const UserContext = createContext<{ id: string; name: string } | null>(null)
// ========== 中间件 ==========
const authMiddleware = (req, next) => {
const token = req.headers?.authorization
if (!token) {
return Response.status(401).json({ error: '需要登录' })
}
const user = verifyToken(token)
UserContext.set(user)
return next(req)
}
// ========== 路由器 ==========
const blogRouter = Router()
// 文章列表
blogRouter.get('/?<page?:int>&<limit?:int>').use((req) => {
const { page = 1, limit = 10 } = req.query
const articles = getArticles({ page, limit })
return Response.json({
articles,
pagination: { page, limit, total: getTotalArticles() }
})
})
// 文章详情
blogRouter.get('/<slug:string>').use((req) => {
const article = getArticleBySlug(req.params.slug)
if (!article) {
return Response.status(404).json({ error: '文章不存在' })
}
return Response.json({ article })
})
// 创建文章 (需要认证)
blogRouter.post('/', { body: CreateArticleInput })
.use(authMiddleware)
.use((req) => {
const user = UserContext.assert()
const article = createArticle({
...req.body,
authorId: user.id
})
return Response.status(201).json({ article })
})
// ========== 主应用 ==========
const app = Http({ basenames: ['/api'] })
// 全局中间件
app.use(logger)
app.use(errorHandler)
// 挂载路由
app.route('/blog').use(blogRouter)
// 健康检查
app.get('/health').use(() => {
return Response.json({
status: 'ok',
timestamp: Date.now()
})
})
// 启动服务器
app.listen(3000, () => {
console.log('🎉 博客 API 启动成功!')
console.log('📝 文章列表: http://localhost:3000/api/blog')
console.log('💚 健康检查: http://localhost:3000/api/health')
})测试
使用 supertest 测试
typescript
import { Http, Response } from 'farrow-http'
import request from 'supertest'
const app = Http()
app.get('/hello').use(() => Response.json({ message: 'Hello' }))
// 获取服务器实例 (不启动)
const server = app.server()
describe('API 测试', () => {
test('GET /hello', async () => {
const response = await request(server)
.get('/hello')
.expect(200)
expect(response.body).toEqual({ message: 'Hello' })
})
})测试带认证的路由
typescript
test('受保护的路由', async () => {
const token = generateTestToken()
await request(server)
.get('/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200)
})测试 Schema 验证
typescript
test('验证失败返回 400', async () => {
await request(server)
.post('/users')
.send({ name: 'Alice' }) // 缺少 email
.expect(400)
})测试中间件
typescript
describe('认证中间件', () => {
test('没有 token 返回 401', async () => {
await request(server)
.get('/protected')
.expect(401)
})
test('无效 token 返回 401', async () => {
await request(server)
.get('/protected')
.set('Authorization', 'Bearer invalid-token')
.expect(401)
})
test('有效 token 返回 200', async () => {
const token = generateValidToken()
await request(server)
.get('/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200)
})
})测试 Context
typescript
import { createContainer } from 'farrow-pipeline'
test('Context 隔离', async () => {
const UserContext = createContext<User | null>(null)
const testUser = { id: '123', name: 'Test User' }
const testContainer = createContainer()
UserContext.set(testUser)
// 在测试容器中运行
const result = await pipeline.run(input, { container: testContainer })
expect(result.user).toEqual(testUser)
})进阶技巧
技巧 1: 依赖注入模式
typescript
// 定义依赖接口
interface Database {
query: (sql: string) => Promise<any>
}
const DatabaseContext = createContext<Database | null>(null)
// 在应用启动时注入
app.use((req, next) => {
const db = createDatabaseConnection()
DatabaseContext.set(db)
return next(req)
})
// 在路由中使用
app.get('/users').use(async () => {
const db = DatabaseContext.assert()
const users = await db.query('SELECT * FROM users')
return Response.json({ users })
})
// 测试时注入 Mock
const mockDB = { query: jest.fn() }
DatabaseContext.set(mockDB)技巧 2: 响应拦截
typescript
// 为所有 JSON 响应添加时间戳
app.capture('json', (jsonBody) => {
return Response.json({
data: jsonBody.value,
timestamp: new Date().toISOString(),
version: 'v1'
})
})
// 为所有文件响应添加缓存头
app.capture('file', (fileBody) => {
return Response.file(fileBody.value, fileBody.options)
.header('Cache-Control', 'public, max-age=3600')
})技巧 3: 自定义装饰器模式
typescript
// 创建装饰器工厂
function withCache(ttl: number) {
const cache = new Map()
return (handler) => {
return (req, next) => {
const key = req.pathname + JSON.stringify(req.query)
const cached = cache.get(key)
if (cached && Date.now() < cached.expiresAt) {
return Response.json(cached.data)
}
const response = handler(req, next)
if (response.info.body?.type === 'json') {
cache.set(key, {
data: response.info.body.value,
expiresAt: Date.now() + ttl
})
}
return response
}
}
}
// 使用
app.get('/expensive-data').use(
withCache(60000), // 缓存 1 分钟
async () => {
const data = await fetchExpensiveData()
return Response.json(data)
}
)技巧 4: 自动重试
typescript
function withRetry(maxRetries: number) {
return async (handler) => {
return async (req, next) => {
let lastError
for (let i = 0; i < maxRetries; i++) {
try {
return await handler(req, next)
} catch (error) {
lastError = error
await delay(Math.pow(2, i) * 1000) // 指数退避
}
}
throw lastError
}
}
}
// 使用
app.get('/unreliable').use(
withRetry(3),
async () => {
const data = await unreliableAPI()
return Response.json(data)
}
)技巧 5: GraphQL 集成
typescript
import { graphqlHTTP } from 'express-graphql'
import { buildSchema } from 'graphql'
const schema = buildSchema(`
type Query {
hello: String
}
`)
const root = {
hello: () => 'Hello world!'
}
app.post('/graphql').use((req) => {
// 适配到 Express middleware
return new Promise((resolve) => {
graphqlHTTP({
schema,
rootValue: root,
graphiql: true
})(req, {
json: (data) => resolve(Response.json(data))
}, () => {})
})
})技巧 6: WebSocket 集成
typescript
import { WebSocketServer } from 'ws'
const app = Http()
const server = app.server()
// 创建 WebSocket 服务器
const wss = new WebSocketServer({ server })
wss.on('connection', (ws) => {
console.log('WebSocket 连接建立')
ws.on('message', (message) => {
console.log('收到消息:', message)
ws.send(`Echo: ${message}`)
})
ws.on('close', () => {
console.log('WebSocket 连接关闭')
})
})
// HTTP 路由
app.get('/').use(() => {
return Response.html(`
<script>
const ws = new WebSocket('ws://localhost:3000')
ws.onmessage = (e) => console.log(e.data)
ws.send('Hello')
</script>
`)
})
app.listen(3000)技巧 7: Server-Sent Events
typescript
import { Readable } from 'stream'
app.get('/events').use(() => {
const stream = new Readable({
read() {}
})
// 定期发送事件
const interval = setInterval(() => {
stream.push(`data: ${JSON.stringify({
timestamp: Date.now(),
message: 'Hello'
})}\n\n`)
}, 1000)
// 清理
stream.on('close', () => {
clearInterval(interval)
})
return Response
.stream(stream)
.header('Content-Type', 'text/event-stream')
.header('Cache-Control', 'no-cache')
.header('Connection', 'keep-alive')
})技巧 8: 文件上传
typescript
import formidable from 'formidable'
import fs from 'fs'
app.post('/upload').use((req) => {
return new Promise((resolve, reject) => {
const form = formidable({
uploadDir: './uploads',
keepExtensions: true
})
form.parse(req, (err, fields, files) => {
if (err) {
reject(err)
return
}
resolve(Response.json({
fields,
files: Object.values(files).map((file: any) => ({
name: file.originalFilename,
path: file.filepath,
size: file.size
}))
}))
})
})
})性能优化
1. 启用压缩
typescript
import compression from 'compression'
// 使用 compression 中间件
app.use(async (req, next) => {
// 适配 compression
return new Promise((resolve) => {
compression()(req, {
// 响应对象适配
}, () => {
resolve(next(req))
})
})
})2. 响应缓存
typescript
const responseCache = new Map()
app.use((req, next) => {
if (req.method !== 'GET') {
return next(req)
}
const key = req.pathname
const cached = responseCache.get(key)
if (cached) {
return cached
}
const response = next(req)
responseCache.set(key, response)
setTimeout(() => {
responseCache.delete(key)
}, 60000) // 1 分钟后清除
return response
})3. 数据库连接池
typescript
import { Pool } from 'pg'
const pool = new Pool({
host: 'localhost',
database: 'mydb',
max: 20,
idleTimeoutMillis: 30000
})
const DBContext = createContext(pool)
app.use((req, next) => {
DBContext.set(pool)
return next(req)
})生产环境部署
PM2 配置
javascript
// ecosystem.config.js
module.exports = {
apps: [{
name: 'api-server',
script: './dist/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
}
}]
}Docker 配置
dockerfile
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]Nginx 反向代理
nginx
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}总结
恭喜你! 🎉 你已经掌握了 farrow-http 的核心概念:
- 类型安全路由 - URL 模式自动推导类型
- Schema 验证 - 声明式数据验证
- 函数式中间件 - 洋葱模型,强制返回
- Context 系统 - 请求级状态管理
- 模块化路由 - 清晰的代码组织
为什么 farrow-http 让人上瘾?
- ✅ 类型安全 = 编译期发现错误
- ✅ 自动验证 = 告别手写检查
- ✅ 函数式 = 可预测、可测试
- ✅ 优雅 API = 写代码也是享受
下一步探索
- 🔍 深入 farrow-schema - 掌握复杂验证
- 🔧 了解 farrow-pipeline - 探索中间件系统
- 🚀 查看 API 参考 - 完整 API 文档
记住: 好的代码不仅要能运行,还要让人愉悦。farrow-http 的设计理念就是让 Web 开发变得更加优雅和高效。
现在就开始你的 farrow-http 之旅吧! 让类型安全成为你的超能力! 🦸♂️
"Write code that humans can understand." - Martin Fowler
愿你的每一行代码都充满智慧与美感! ✨
