@vafast/api-client 0.2.2 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,22 +1,103 @@
1
1
  # @vafast/api-client
2
2
 
3
- 类型安全的 Eden 风格 API 客户端,支持从 vafast 路由自动推断类型。
3
+ 类型安全的 Eden 风格 API 客户端,基于中间件架构,支持从 vafast 路由自动推断类型。
4
+
5
+ ## 特性
6
+
7
+ - 🎯 **类型安全** - 从 vafast 路由自动推断,或手动定义契约
8
+ - 🧅 **中间件架构** - Koa 风格洋葱模型,灵活组合
9
+ - 🔄 **内置重试** - 支持指数退避、条件重试
10
+ - ⏱️ **超时控制** - 请求级别和全局超时
11
+ - 📡 **SSE 支持** - 流式响应、自动重连
12
+ - 🎨 **Go 风格错误** - `{ data, error }` 统一处理
4
13
 
5
14
  ## 安装
6
15
 
7
16
  ```bash
8
- npm install @vafast/api-client vafast
17
+ npm install @vafast/api-client
9
18
  ```
10
19
 
11
20
  ## 快速开始
12
21
 
13
- ### 方式 1:从 vafast 路由自动推断类型(推荐)
22
+ ```typescript
23
+ import { createClient, eden } from '@vafast/api-client'
24
+
25
+ // 1. 创建客户端
26
+ const client = createClient('http://localhost:3000')
27
+ .headers({ 'Authorization': 'Bearer token' })
28
+ .timeout(30000)
29
+
30
+ // 2. 类型包装
31
+ const api = eden<Api>(client)
32
+
33
+ // 3. 发起请求
34
+ const { data, error } = await api.users.get({ page: 1 })
35
+
36
+ if (error) {
37
+ console.error(`错误 ${error.code}: ${error.message}`)
38
+ return
39
+ }
40
+
41
+ console.log(data.users)
42
+ ```
43
+
44
+ ## 核心 API
45
+
46
+ ### createClient(config)
47
+
48
+ 创建 HTTP 客户端实例,支持两种方式:
49
+
50
+ ```typescript
51
+ // 方式 1:只传 baseURL(简单场景)
52
+ const client = createClient('http://localhost:3000')
53
+ .timeout(30000)
54
+ .use(authMiddleware)
55
+
56
+ // 方式 2:传配置对象(推荐,配置集中)
57
+ const client = createClient({
58
+ baseURL: 'http://localhost:3000',
59
+ timeout: 30000,
60
+ headers: { 'X-App-Id': 'my-app' }
61
+ }).use(authMiddleware)
62
+ ```
63
+
64
+ **配置对象类型:**
65
+
66
+ ```typescript
67
+ interface ClientConfig {
68
+ baseURL: string
69
+ timeout?: number // 默认 30000ms
70
+ headers?: Record<string, string>
71
+ }
72
+ ```
73
+
74
+ **链式方法:**
75
+
76
+ ```typescript
77
+ const client = createClient({ baseURL: '/api', timeout: 30000 })
78
+ .headers({ 'X-App-Id': 'my-app' }) // 追加默认请求头
79
+ .timeout(60000) // 覆盖超时配置
80
+ .use(authMiddleware) // 添加中间件
81
+ .use(retryMiddleware({ count: 3 }))
82
+ ```
83
+
84
+ ### eden<T>(client)
85
+
86
+ 将 Client 实例包装为类型安全的 API 调用。
87
+
88
+ ```typescript
89
+ type Api = InferEden<typeof routes> // 从 vafast 路由推断
90
+ const api = eden<Api>(client)
91
+ ```
92
+
93
+ ## 类型定义
94
+
95
+ ### 方式 1:从 vafast 路由自动推断(推荐)
14
96
 
15
97
  ```typescript
16
98
  // ============= 服务端 =============
17
99
  import { defineRoute, defineRoutes, Type, Server } from 'vafast'
18
100
 
19
- // 定义路由(使用 as const 保留字面量类型)
20
101
  const routeDefinitions = [
21
102
  defineRoute({
22
103
  method: 'GET',
@@ -38,28 +119,25 @@ const routeDefinitions = [
38
119
  })
39
120
  ] as const
40
121
 
41
- // 创建服务器
42
122
  const routes = defineRoutes(routeDefinitions)
43
123
  const server = new Server(routes)
44
124
 
45
125
  // ============= 客户端 =============
46
- import { eden, InferEden } from '@vafast/api-client'
126
+ import { createClient, eden, InferEden } from '@vafast/api-client'
47
127
 
48
- // 自动推断类型
49
128
  type Api = InferEden<typeof routeDefinitions>
50
- const api = eden<Api>('http://localhost:3000')
129
+
130
+ const client = createClient('http://localhost:3000')
131
+ const api = eden<Api>(client)
51
132
 
52
133
  // ✅ 完全类型安全
53
- const { data } = await api.users.get({ page: 1 }) // query 有类型提示
54
- const { data: user } = await api.users({ id: '123' }).get() // 动态参数
134
+ const { data } = await api.users.get({ page: 1 })
135
+ const { data: user } = await api.users({ id: '123' }).get()
55
136
  ```
56
137
 
57
- ### 方式 2:手动定义契约(非 vafast API)
138
+ ### 方式 2:手动定义契约
58
139
 
59
140
  ```typescript
60
- import { eden } from '@vafast/api-client'
61
-
62
- // 手动定义契约类型
63
141
  type MyApi = {
64
142
  users: {
65
143
  get: { query: { page: number }; return: { users: User[]; total: number } }
@@ -72,40 +150,152 @@ type MyApi = {
72
150
  }
73
151
  }
74
152
 
75
- const api = eden<MyApi>('https://api.example.com')
153
+ const api = eden<MyApi>(createClient('https://api.example.com'))
154
+ ```
76
155
 
77
- // 调用方式完全相同
78
- const { data } = await api.users.get({ page: 1 })
79
- const { data: user } = await api.users({ id: '123' }).get()
156
+ ## 中间件
157
+
158
+ ### 内置中间件
159
+
160
+ ```typescript
161
+ import {
162
+ createClient,
163
+ retryMiddleware,
164
+ timeoutMiddleware,
165
+ loggerMiddleware
166
+ } from '@vafast/api-client'
167
+
168
+ const client = createClient('http://localhost:3000')
169
+ // 重试中间件
170
+ .use(retryMiddleware({
171
+ count: 3, // 最大重试次数
172
+ delay: 1000, // 初始延迟
173
+ backoff: 2, // 退避倍数
174
+ on: [500, 502, 503, 504], // 触发重试的状态码
175
+ shouldRetry: (ctx, res) => true // 自定义重试条件
176
+ }))
177
+ // 超时中间件
178
+ .use(timeoutMiddleware(5000))
179
+ // 日志中间件
180
+ .use(loggerMiddleware({
181
+ prefix: '[API]',
182
+ onRequest: (ctx) => console.log('请求:', ctx.method, ctx.url),
183
+ onResponse: (res) => console.log('响应:', res.status)
184
+ }))
185
+ ```
186
+
187
+ ### 自定义中间件
188
+
189
+ ```typescript
190
+ import { defineMiddleware } from '@vafast/api-client'
191
+
192
+ // 认证中间件
193
+ const authMiddleware = defineMiddleware('auth', async (ctx, next) => {
194
+ const token = localStorage.getItem('token')
195
+ if (token) {
196
+ ctx.headers.set('Authorization', `Bearer ${token}`)
197
+ }
198
+
199
+ const response = await next()
200
+
201
+ // Token 过期处理
202
+ if (response.status === 401) {
203
+ // 刷新 token 逻辑...
204
+ }
205
+
206
+ return response
207
+ })
208
+
209
+ // 动态 header 中间件
210
+ const dynamicHeaderMiddleware = defineMiddleware('dynamic-header', async (ctx, next) => {
211
+ // 从路由或 store 获取动态值
212
+ const orgId = getCurrentOrganizationId()
213
+ const appId = getCurrentAppId()
214
+
215
+ ctx.headers.set('organization-id', orgId)
216
+ ctx.headers.set('app-id', appId)
217
+
218
+ return next()
219
+ })
220
+
221
+ const client = createClient('http://localhost:3000')
222
+ .use(authMiddleware)
223
+ .use(dynamicHeaderMiddleware)
80
224
  ```
81
225
 
82
- ## 调用方式
226
+ ### 中间件执行顺序
227
+
228
+ 中间件按照洋葱模型执行:
229
+
230
+ ```
231
+ 请求 → auth → retry → timeout → [fetch] → timeout → retry → auth → 响应
232
+ ```
233
+
234
+ ## 多服务配置
235
+
236
+ 针对不同后端服务创建独立客户端:
83
237
 
84
238
  ```typescript
85
- // GET 请求 + query 参数
86
- const { data, error } = await api.users.get({ page: 1, limit: 10 })
239
+ // 公共配置
240
+ const AUTH_API = { baseURL: '/authRestfulApi', timeout: 30000 }
241
+ const ONES_API = { baseURL: '/restfulApi', timeout: 30000 }
242
+ const BILLING_API = { baseURL: '/billingRestfulApi', timeout: 30000 }
243
+
244
+ // Auth 服务
245
+ const authClient = createClient(AUTH_API)
246
+
247
+ // API 服务(需要额外 header)
248
+ const apiClient = createClient(ONES_API).use(dynamicHeaderMiddleware)
249
+
250
+ // Billing 服务
251
+ const billingClient = createClient(BILLING_API).use(billingHeaderMiddleware)
87
252
 
88
- // POST 请求 + body
89
- const { data, error } = await api.users.post({ name: 'John', email: 'john@example.com' })
253
+ // 使用 CLI 生成的类型安全客户端
254
+ import { createApiClient as createAuthClient } from './types/auth.generated'
255
+ import { createApiClient as createOnesClient } from './types/ones.generated'
256
+ import { createApiClient as createBillingClient } from './types/billing.generated'
90
257
 
91
- // 动态路径参数
92
- const { data, error } = await api.users({ id: '123' }).get()
93
- const { data, error } = await api.users({ id: '123' }).put({ name: 'Jane' })
94
- const { data, error } = await api.users({ id: '123' }).delete()
258
+ export const auth = createAuthClient(authClient)
259
+ export const ones = createOnesClient(apiClient)
260
+ export const billing = createBillingClient(billingClient)
95
261
 
96
- // 嵌套路径
97
- const { data, error } = await api.users({ id: '123' }).posts.get()
98
- const { data, error } = await api.users({ id: '123' }).posts({ id: '456' }).get()
262
+ // 使用示例
263
+ const { data, error } = await ones.users.find.post({ current: 1, pageSize: 10 })
264
+ ```
265
+
266
+ ## 请求级配置
267
+
268
+ ```typescript
269
+ // 单次请求覆盖配置
270
+ const { data, error } = await api.users.get(
271
+ { page: 1 },
272
+ {
273
+ headers: { 'X-Request-Id': 'xxx' }, // 额外 header
274
+ timeout: 5000, // 请求超时
275
+ signal: controller.signal // 取消信号
276
+ }
277
+ )
99
278
  ```
100
279
 
101
280
  ## Go 风格错误处理
102
281
 
282
+ 所有请求返回 `{ data, error }` 格式:
283
+
103
284
  ```typescript
104
285
  const { data, error } = await api.users.get()
105
286
 
106
287
  if (error) {
107
288
  // error: { code: number; message: string }
108
- console.error(`错误 ${error.code}: ${error.message}`)
289
+ switch (error.code) {
290
+ case 401:
291
+ redirectToLogin()
292
+ break
293
+ case 403:
294
+ showPermissionDenied()
295
+ break
296
+ default:
297
+ showError(error.message)
298
+ }
109
299
  return
110
300
  }
111
301
 
@@ -113,56 +303,10 @@ if (error) {
113
303
  console.log(data.users)
114
304
  ```
115
305
 
116
- ## 配置选项
117
-
118
- ```typescript
119
- const api = eden<Api>('http://localhost:3000', {
120
- // 默认请求头
121
- headers: {
122
- 'Authorization': 'Bearer token123'
123
- },
124
-
125
- // 请求超时(毫秒)
126
- timeout: 30000,
127
-
128
- // 请求拦截器
129
- onRequest: async (request) => {
130
- // 可以修改请求
131
- return request
132
- },
133
-
134
- // 响应拦截器
135
- onResponse: async (response) => {
136
- // 可以修改响应
137
- return response
138
- },
139
-
140
- // 错误回调
141
- onError: (error) => {
142
- console.error('API Error:', error.code, error.message)
143
- }
144
- })
145
- ```
146
-
147
306
  ## SSE 流式响应
148
307
 
149
308
  ```typescript
150
- import { defineRoute, Type } from 'vafast'
151
-
152
- // 服务端定义 SSE 路由
153
- const routeDefinitions = [
154
- defineRoute({
155
- method: 'GET',
156
- path: '/chat/stream',
157
- schema: { query: Type.Object({ prompt: Type.String() }) },
158
- handler: async function* ({ query }) {
159
- yield { data: { text: 'Hello' } }
160
- yield { data: { text: 'World' } }
161
- }
162
- })
163
- ] as const
164
-
165
- // 客户端(手动标记 SSE)
309
+ // 定义 SSE 类型
166
310
  type Api = {
167
311
  chat: {
168
312
  stream: {
@@ -175,26 +319,21 @@ type Api = {
175
319
  }
176
320
  }
177
321
 
178
- const api = eden<Api>('http://localhost:3000')
322
+ const api = eden<Api>(client)
179
323
 
180
324
  // 订阅 SSE 流
181
325
  const subscription = api.chat.stream.subscribe(
182
326
  { prompt: 'Hello' },
183
327
  {
184
- onMessage: (data) => {
185
- console.log('收到消息:', data.text)
186
- },
187
- onError: (error) => {
188
- console.error('错误:', error.message)
189
- },
328
+ onMessage: (data) => console.log('收到:', data.text),
329
+ onError: (error) => console.error('错误:', error),
190
330
  onOpen: () => console.log('连接建立'),
191
331
  onClose: () => console.log('连接关闭'),
192
- onReconnect: (attempt, max) => console.log(`重连中 ${attempt}/${max}`),
193
- onMaxReconnects: () => console.log('达到最大重连次数')
332
+ onReconnect: (attempt, max) => console.log(`重连 ${attempt}/${max}`)
194
333
  },
195
334
  {
196
- reconnectInterval: 3000, // 重连间隔
197
- maxReconnects: 5 // 最大重连次数
335
+ reconnectInterval: 3000,
336
+ maxReconnects: 5
198
337
  }
199
338
  )
200
339
 
@@ -213,21 +352,131 @@ const promise = api.users.get({ page: 1 }, { signal: controller.signal })
213
352
  controller.abort()
214
353
  ```
215
354
 
216
- ## API
355
+ ---
356
+
357
+ ## 最佳实践:HTTP 状态码 vs 全部 200
358
+
359
+ ### ✅ 推荐:使用 HTTP 状态码
360
+
361
+ `@vafast/api-client` 设计为使用 HTTP 状态码判断请求成功/失败:
362
+
363
+ | HTTP 状态码 | 含义 |
364
+ |------------|------|
365
+ | 2xx | 成功 |
366
+ | 400 | 客户端错误(参数错误) |
367
+ | 401 | 未认证(Token 无效/过期) |
368
+ | 403 | 无权限 |
369
+ | 404 | 资源不存在 |
370
+ | 5xx | 服务器错误 |
371
+
372
+ **后端响应示例:**
373
+
374
+ ```
375
+ HTTP 401 Unauthorized
376
+
377
+ {
378
+ "code": 10001,
379
+ "message": "Token 已过期"
380
+ }
381
+ ```
382
+
383
+ ### ❌ 不推荐:全部返回 200 + success 字段
384
+
385
+ ```json
386
+ HTTP 200 OK
387
+
388
+ {
389
+ "success": false,
390
+ "code": 10001,
391
+ "message": "Token 已过期"
392
+ }
393
+ ```
394
+
395
+ ### 为什么 HTTP 状态码更好?
396
+
397
+ | 方面 | HTTP 状态码 | 全部 200 |
398
+ |------|------------|----------|
399
+ | **监控告警** | 自动识别错误率 | 全是 200,无法识别 |
400
+ | **浏览器调试** | DevTools 红色标记失败 | 全绿,难以调试 |
401
+ | **CDN 缓存** | 不会缓存错误响应 | 可能错误缓存 |
402
+ | **重试策略** | 503 重试,400 不重试 | 无法区分 |
403
+ | **协议语义** | 符合 HTTP 标准 | 违背设计意图 |
404
+
405
+ ### 兼容旧系统
406
+
407
+ 如果后端暂时无法修改,使用中间件做兼容:
408
+
409
+ ```typescript
410
+ const legacyMiddleware = defineMiddleware('legacy', async (ctx, next) => {
411
+ const response = await next()
412
+
413
+ // 兼容旧的 { success: false } 格式
414
+ if (response.status === 200 && response.data?.success === false) {
415
+ response.error = {
416
+ code: response.data.code ?? 500,
417
+ message: response.data.message ?? '请求失败'
418
+ }
419
+ response.data = null
420
+ }
421
+
422
+ return response
423
+ })
424
+
425
+ const client = createClient('http://localhost:3000')
426
+ .use(legacyMiddleware)
427
+ ```
428
+
429
+ > ⚠️ 这只是过渡方案,建议尽快让后端返回正确的 HTTP 状态码。
430
+
431
+ ---
432
+
433
+ ## API 参考
434
+
435
+ ### createClient(config)
436
+
437
+ 创建 HTTP 客户端。
438
+
439
+ **参数:**
440
+ - `config: string | ClientConfig` - baseURL 字符串或配置对象
441
+
442
+ **ClientConfig:**
443
+ ```typescript
444
+ interface ClientConfig {
445
+ baseURL: string
446
+ timeout?: number // 默认 30000ms
447
+ headers?: Record<string, string>
448
+ }
449
+ ```
217
450
 
218
- ### `eden<T>(baseURL, config?)`
451
+ **返回值(链式):**
452
+ - `.headers(headers)` - 追加默认请求头
453
+ - `.timeout(ms)` - 设置默认超时
454
+ - `.use(middleware)` - 添加中间件
455
+ - `.request(method, path, data?, config?)` - 发起请求
219
456
 
220
- 创建 API 客户端实例。
457
+ ### eden<T>(client)
221
458
 
222
- - `baseURL` - API 基础 URL
223
- - `config` - 可选配置
224
- - `headers` - 默认请求头
225
- - `timeout` - 请求超时(毫秒)
226
- - `onRequest` - 请求拦截器
227
- - `onResponse` - 响应拦截器
228
- - `onError` - 错误回调
459
+ 创建类型安全的 API 客户端。
460
+
461
+ ### defineMiddleware(name, fn)
462
+
463
+ 创建命名中间件。
464
+
465
+ ```typescript
466
+ const myMiddleware = defineMiddleware('my-middleware', async (ctx, next) => {
467
+ // 请求前处理
468
+ console.log('请求:', ctx.method, ctx.url)
469
+
470
+ const response = await next()
471
+
472
+ // 响应后处理
473
+ console.log('响应:', response.status)
474
+
475
+ return response
476
+ })
477
+ ```
229
478
 
230
- ### `InferEden<T>`
479
+ ### InferEden<T>
231
480
 
232
481
  从 `defineRoute` 数组推断 Eden 契约类型。
233
482