Skip to content

实战与进阶

完整实战项目:博客 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 的核心概念:

  1. 类型安全路由 - URL 模式自动推导类型
  2. Schema 验证 - 声明式数据验证
  3. 函数式中间件 - 洋葱模型,强制返回
  4. Context 系统 - 请求级状态管理
  5. 模块化路由 - 清晰的代码组织

为什么 farrow-http 让人上瘾?

  • 类型安全 = 编译期发现错误
  • 自动验证 = 告别手写检查
  • 函数式 = 可预测、可测试
  • 优雅 API = 写代码也是享受

下一步探索

记住: 好的代码不仅要能运行,还要让人愉悦。farrow-http 的设计理念就是让 Web 开发变得更加优雅和高效。

现在就开始你的 farrow-http 之旅吧! 让类型安全成为你的超能力! 🦸‍♂️


"Write code that humans can understand." - Martin Fowler

愿你的每一行代码都充满智慧与美感! ✨

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