1000fetches 0.2.0 → 0.2.2
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/LICENSE +1 -1
- package/README.md +94 -68
- package/dist/client-C7dvgNnD.js +2 -0
- package/dist/client-C7dvgNnD.js.map +1 -0
- package/dist/client-DOv6m2TJ.mjs +2 -0
- package/dist/client-DOv6m2TJ.mjs.map +1 -0
- package/dist/client.d.ts +22 -0
- package/dist/core.d.ts +57 -0
- package/dist/errors.d.ts +76 -0
- package/dist/http.cjs +2 -0
- package/dist/http.cjs.map +1 -0
- package/dist/http.d.ts +9 -0
- package/dist/http.mjs +2 -0
- package/dist/http.mjs.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +15 -346
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/schema.d.ts +35 -0
- package/dist/types.d.ts +145 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/path.d.ts +73 -0
- package/dist/utils/streaming.d.ts +9 -0
- package/docs/API.md +754 -0
- package/docs/BEST_PRACTICES.md +606 -0
- package/docs/MIGRATION.md +719 -0
- package/package.json +40 -53
- package/dist/index.cjs.js +0 -2
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.es.js +0 -787
- package/dist/index.es.js.map +0 -1
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
# Best Practices Guide
|
|
2
|
+
|
|
3
|
+
This guide provides recommendations for using 1000fetches effectively in production applications.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Client Configuration](#client-configuration)
|
|
8
|
+
- [Error Handling](#error-handling)
|
|
9
|
+
- [Schema Validation](#schema-validation)
|
|
10
|
+
- [Middleware](#middleware)
|
|
11
|
+
- [Performance Optimization](#performance-optimization)
|
|
12
|
+
- [Testing](#testing)
|
|
13
|
+
- [Security](#security)
|
|
14
|
+
- [Monitoring](#monitoring)
|
|
15
|
+
|
|
16
|
+
## Client Configuration
|
|
17
|
+
|
|
18
|
+
### ✅ DO: Create a Single Client Instance
|
|
19
|
+
|
|
20
|
+
Create one client instance per API and reuse it throughout your application.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createHttpClient } from '1000fetches'
|
|
24
|
+
|
|
25
|
+
export const apiClient = createHttpClient({
|
|
26
|
+
baseUrl: process.env.API_BASE_URL,
|
|
27
|
+
timeout: 10_000,
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'User-Agent': 'MyApp/1.0.0',
|
|
31
|
+
},
|
|
32
|
+
retry: {
|
|
33
|
+
maxRetries: 3,
|
|
34
|
+
retryDelay: 1_000,
|
|
35
|
+
retryStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### ✅ DO: Use Environment-Specific Configuration
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const config = {
|
|
44
|
+
development: {
|
|
45
|
+
baseUrl: 'http://localhost:3000/api',
|
|
46
|
+
timeout: 30_000, // Longer timeout for development
|
|
47
|
+
},
|
|
48
|
+
production: {
|
|
49
|
+
baseUrl: 'https://api.myapp.com',
|
|
50
|
+
timeout: 10_000,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const environment =
|
|
55
|
+
process.env.NODE_ENV === 'production' ? 'production' : 'development'
|
|
56
|
+
|
|
57
|
+
export const apiClient = createHttpClient(config[environment])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### ❌ DON'T: Create Multiple Clients for the Same API
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Bad: Creates unnecessary overhead
|
|
64
|
+
const userClient = createHttpClient({ baseUrl: 'https://api.example.com' })
|
|
65
|
+
const postClient = createHttpClient({ baseUrl: 'https://api.example.com' })
|
|
66
|
+
|
|
67
|
+
// Good: Use one client with different endpoints
|
|
68
|
+
const apiClient = createHttpClient({ baseUrl: 'https://api.example.com' })
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Error Handling
|
|
72
|
+
|
|
73
|
+
### ✅ DO: Handle Specific Error Types
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import {
|
|
77
|
+
HttpError,
|
|
78
|
+
NetworkError,
|
|
79
|
+
TimeoutError,
|
|
80
|
+
SchemaValidationError,
|
|
81
|
+
} from '1000fetches'
|
|
82
|
+
|
|
83
|
+
async function fetchUser(id: string) {
|
|
84
|
+
try {
|
|
85
|
+
return await apiClient.get(`/users/${id}`)
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof HttpError) {
|
|
88
|
+
switch (error.status) {
|
|
89
|
+
case 404:
|
|
90
|
+
throw new UserNotFoundError(`User ${id} not found`)
|
|
91
|
+
case 403:
|
|
92
|
+
throw new UnauthorizedError('Access denied')
|
|
93
|
+
default:
|
|
94
|
+
throw new ApiError(`API error: ${error.status}`)
|
|
95
|
+
}
|
|
96
|
+
} else if (error instanceof NetworkError) {
|
|
97
|
+
throw new ConnectivityError('Network connection failed')
|
|
98
|
+
} else if (error instanceof TimeoutError) {
|
|
99
|
+
throw new TimeoutError('Request timed out')
|
|
100
|
+
} else {
|
|
101
|
+
throw error
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### ✅ DO: Implement Retry Logic for Idempotent Operations
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Good: Retry safe operations
|
|
111
|
+
const user = await apiClient.get('/users/1', {
|
|
112
|
+
retry: {
|
|
113
|
+
maxRetries: 3,
|
|
114
|
+
retryDelay: 1_000,
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Be careful: Only retry idempotent operations
|
|
119
|
+
const newUser = await apiClient.post('/users', userData, {
|
|
120
|
+
retry: false,
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Schema Validation
|
|
125
|
+
|
|
126
|
+
### ✅ DO: Validate Response Data
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { z } from 'zod'
|
|
130
|
+
|
|
131
|
+
const userSchema = z.object({
|
|
132
|
+
id: z.number(),
|
|
133
|
+
name: z.string(),
|
|
134
|
+
email: z.string(),
|
|
135
|
+
age: z.number().optional(),
|
|
136
|
+
createdAt: z.iso.datetime(),
|
|
137
|
+
updatedAt: z.iso.datetime(),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
await apiClient.get(`/users/${id}`).schema(userSchema)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### ✅ DO: Create Reusable Schema Compositions
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
const BaseEntitySchema = z.object({
|
|
147
|
+
id: z.number(),
|
|
148
|
+
createdAt: z.iso.datetime(),
|
|
149
|
+
updatedAt: z.iso.datetime(),
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const UserSchema = BaseEntitySchema.extend({
|
|
153
|
+
name: z.string(),
|
|
154
|
+
email: z.email(),
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const PostSchema = BaseEntitySchema.extend({
|
|
158
|
+
title: z.string(),
|
|
159
|
+
content: z.string(),
|
|
160
|
+
authorId: z.number(),
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Middleware
|
|
165
|
+
|
|
166
|
+
### ✅ DO: Use Middleware for Cross-Cutting Concerns
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const apiClient = createHttpClient({
|
|
170
|
+
baseUrl: 'https://api.example.com',
|
|
171
|
+
onRequestMiddleware: async context => {
|
|
172
|
+
const token = await getAuthToken()
|
|
173
|
+
console.log(`→ ${context.method} ${context.url}`)
|
|
174
|
+
context.headers.set('Authorization', `Bearer ${token}`)
|
|
175
|
+
return context
|
|
176
|
+
},
|
|
177
|
+
onResponseMiddleware: async response => {
|
|
178
|
+
console.log(`← ${response.status} ${response.method} ${response.url}`)
|
|
179
|
+
return response
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### ✅ DO: Handle Middleware Errors Gracefully
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const apiClient = createHttpClient({
|
|
188
|
+
baseUrl: 'https://api.example.com',
|
|
189
|
+
onRequestMiddleware: async context => {
|
|
190
|
+
try {
|
|
191
|
+
const token = await getAuthToken()
|
|
192
|
+
context.headers.set('Authorization', `Bearer ${token}`)
|
|
193
|
+
return context
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Log the error but don't fail the request
|
|
196
|
+
console.warn('Failed to get auth token:', error)
|
|
197
|
+
return context
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### ✅ DO: Treat Response Status Rewrites as Explicit Recovery
|
|
204
|
+
|
|
205
|
+
Response middleware is authoritative. If it returns a different `status`, that changed status controls whether the request resolves or throws `HttpError`.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const apiClient = createHttpClient({
|
|
209
|
+
baseUrl: 'https://api.example.com',
|
|
210
|
+
onResponseMiddleware: async response => {
|
|
211
|
+
if (response.status === 404) {
|
|
212
|
+
return {
|
|
213
|
+
...response,
|
|
214
|
+
status: 200,
|
|
215
|
+
statusText: 'Recovered Not Found',
|
|
216
|
+
data: null,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return response
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
When you only want to normalize an error payload, keep the original status:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const apiClient = createHttpClient({
|
|
229
|
+
baseUrl: 'https://api.example.com',
|
|
230
|
+
onResponseMiddleware: async response => {
|
|
231
|
+
if (response.status >= 400) {
|
|
232
|
+
return {
|
|
233
|
+
...response,
|
|
234
|
+
data: normalizeApiError(response.data),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return response
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### ❌ DON'T: Perform Heavy Operations in Middleware
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Bad: Heavy computation in middleware
|
|
247
|
+
const apiClient = createHttpClient({
|
|
248
|
+
onRequestMiddleware: async context => {
|
|
249
|
+
const heavyResult = await performHeavyComputation() // Slows down all requests
|
|
250
|
+
return context
|
|
251
|
+
},
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Good: Keep middleware lightweight
|
|
255
|
+
const apiClient = createHttpClient({
|
|
256
|
+
onRequestMiddleware: async context => {
|
|
257
|
+
context.headers.set('X-Request-ID', generateRequestId()) // Fast operation
|
|
258
|
+
return context
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Performance Optimization
|
|
264
|
+
|
|
265
|
+
### ✅ DO: Use Appropriate Timeouts
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// Different timeouts for different operations
|
|
269
|
+
const quickClient = createHttpClient({
|
|
270
|
+
baseUrl: 'https://api.example.com',
|
|
271
|
+
timeout: 5_000, // 5 seconds for quick operations
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const longRunningClient = createHttpClient({
|
|
275
|
+
baseUrl: 'https://api.example.com',
|
|
276
|
+
timeout: 60_000, // 60 seconds for heavy operations
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### ✅ DO: Implement Request Cancellation
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
async function searchUsers(query: string, signal?: AbortSignal) {
|
|
284
|
+
return await apiClient.get('/users/search', {
|
|
285
|
+
params: { q: query },
|
|
286
|
+
signal,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const controller = new AbortController()
|
|
291
|
+
const searchPromise = searchUsers('john', controller.signal)
|
|
292
|
+
|
|
293
|
+
// Cancel if needed
|
|
294
|
+
controller.abort()
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Testing
|
|
298
|
+
|
|
299
|
+
### ✅ DO: Mock HTTP Calls in Tests
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { vi } from 'vitest'
|
|
303
|
+
import { apiClient } from '../api/client'
|
|
304
|
+
|
|
305
|
+
vi.mock('../api/client', () => ({
|
|
306
|
+
apiClient: {
|
|
307
|
+
get: vi.fn(),
|
|
308
|
+
post: vi.fn(),
|
|
309
|
+
put: vi.fn(),
|
|
310
|
+
delete: vi.fn(),
|
|
311
|
+
},
|
|
312
|
+
}))
|
|
313
|
+
|
|
314
|
+
describe('UserService', () => {
|
|
315
|
+
it('should fetch user by id', async () => {
|
|
316
|
+
const mockUser = { id: 1, name: 'John', email: 'john@example.com' }
|
|
317
|
+
|
|
318
|
+
vi.mocked(apiClient.get).mockResolvedValue({
|
|
319
|
+
data: mockUser,
|
|
320
|
+
status: 200,
|
|
321
|
+
statusText: 'OK',
|
|
322
|
+
headers: {},
|
|
323
|
+
method: 'GET',
|
|
324
|
+
url: '/users/1',
|
|
325
|
+
raw: new Response(),
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
const user = await UserService.getUser('1')
|
|
329
|
+
|
|
330
|
+
expect(apiClient.get).toHaveBeenCalledWith('/users/1')
|
|
331
|
+
expect(user).toEqual(mockUser)
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### ✅ DO: Test Error Scenarios
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
it('should handle user not found error', async () => {
|
|
340
|
+
vi.mocked(apiClient.get).mockRejectedValue(
|
|
341
|
+
new HttpError(
|
|
342
|
+
'Not Found',
|
|
343
|
+
404,
|
|
344
|
+
'Not Found',
|
|
345
|
+
null,
|
|
346
|
+
new Response(),
|
|
347
|
+
'/users/999',
|
|
348
|
+
'GET'
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
await expect(UserService.getUser('999')).rejects.toThrow('User not found')
|
|
353
|
+
})
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### ✅ DO: Use MSW for Integration Tests
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { http, HttpResponse } from 'msw'
|
|
360
|
+
import { setupServer } from 'msw/node'
|
|
361
|
+
|
|
362
|
+
const server = setupServer(
|
|
363
|
+
http.get('/api/users/:id', ({ params }) => {
|
|
364
|
+
return HttpResponse.json({
|
|
365
|
+
id: Number(params.id),
|
|
366
|
+
name: 'John',
|
|
367
|
+
email: 'john@example.com',
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
beforeAll(() => server.listen())
|
|
373
|
+
afterEach(() => server.resetHandlers())
|
|
374
|
+
afterAll(() => server.close())
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Security
|
|
378
|
+
|
|
379
|
+
### ✅ DO: Validate and Sanitize Input
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
const CreateUserSchema = z.object({
|
|
383
|
+
name: z
|
|
384
|
+
.string()
|
|
385
|
+
.min(1)
|
|
386
|
+
.max(100)
|
|
387
|
+
.regex(/^[a-zA-Z\s]+$/),
|
|
388
|
+
email: z.email().max(255),
|
|
389
|
+
age: z.number().int().min(0).max(120).optional(),
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
async function createUser(userData: unknown) {
|
|
393
|
+
// Validate input before sending
|
|
394
|
+
const validatedData = CreateUserSchema.parse(userData)
|
|
395
|
+
|
|
396
|
+
return await apiClient.post('/users', validatedData)
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### ✅ DO: Configure baseUrl based on Environment
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
const apiClient = createHttpClient({
|
|
404
|
+
baseUrl:
|
|
405
|
+
process.env.NODE_ENV === 'production'
|
|
406
|
+
? 'https://api.myapp.com'
|
|
407
|
+
: 'http://localhost:3000/api',
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### ✅ DO: Handle Sensitive Data Carefully
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
// Don't log sensitive data
|
|
415
|
+
const apiClient = createHttpClient({
|
|
416
|
+
onRequestMiddleware: async context => {
|
|
417
|
+
const safeHeaders = new Headers(context.headers)
|
|
418
|
+
if (safeHeaders.has('Authorization')) {
|
|
419
|
+
safeHeaders.set('Authorization', '[REDACTED]')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const safeContext = {
|
|
423
|
+
...context,
|
|
424
|
+
headers: Object.fromEntries(safeHeaders.entries()),
|
|
425
|
+
}
|
|
426
|
+
console.log('Request:', safeContext)
|
|
427
|
+
return context
|
|
428
|
+
},
|
|
429
|
+
})
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### ❌ DON'T: Store Secrets in Client Code
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// Bad: Hardcoded API key
|
|
436
|
+
const apiClient = createHttpClient({
|
|
437
|
+
baseUrl: 'https://api.example.com',
|
|
438
|
+
headers: {
|
|
439
|
+
'X-API-Key': 'sk-1234567890abcdef', // Never do this!
|
|
440
|
+
},
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
// Good: Use environment variables
|
|
444
|
+
const apiClient = createHttpClient({
|
|
445
|
+
baseUrl: process.env.API_BASE_URL,
|
|
446
|
+
headers: {
|
|
447
|
+
'X-API-Key': process.env.API_KEY,
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Monitoring
|
|
453
|
+
|
|
454
|
+
### ✅ DO: Add Request Tracking
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
const requestTimings = new Map<string, number>()
|
|
458
|
+
|
|
459
|
+
const apiClient = createHttpClient({
|
|
460
|
+
onRequestMiddleware: async context => {
|
|
461
|
+
const requestId = generateRequestId()
|
|
462
|
+
requestTimings.set(requestId, Date.now())
|
|
463
|
+
context.headers.set('X-Request-ID', requestId)
|
|
464
|
+
return context
|
|
465
|
+
},
|
|
466
|
+
onResponseMiddleware: async response => {
|
|
467
|
+
const requestId = response.headers['x-request-id']
|
|
468
|
+
const startTime = requestId ? requestTimings.get(requestId) : undefined
|
|
469
|
+
|
|
470
|
+
if (requestId && startTime !== undefined) {
|
|
471
|
+
const duration = Date.now() - startTime
|
|
472
|
+
console.log(`Request ${requestId} completed in ${duration}ms`)
|
|
473
|
+
|
|
474
|
+
analytics.track('api_request', {
|
|
475
|
+
method: response.method,
|
|
476
|
+
url: response.url,
|
|
477
|
+
status: response.status,
|
|
478
|
+
duration,
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
requestTimings.delete(requestId)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return response
|
|
485
|
+
},
|
|
486
|
+
})
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### ✅ DO: Monitor Error Rates
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const apiClient = createHttpClient({
|
|
493
|
+
onResponseMiddleware: async response => {
|
|
494
|
+
if (response.status >= 400) {
|
|
495
|
+
errorTracker.captureException(
|
|
496
|
+
new Error(`API Error: ${response.status}`),
|
|
497
|
+
{
|
|
498
|
+
extra: {
|
|
499
|
+
url: response.url,
|
|
500
|
+
method: response.method,
|
|
501
|
+
status: response.status,
|
|
502
|
+
data: response.data,
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return response
|
|
509
|
+
},
|
|
510
|
+
})
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### ✅ DO: Set Up Health Checks
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
async function healthCheck() {
|
|
517
|
+
try {
|
|
518
|
+
const response = await apiClient.get('/health', {
|
|
519
|
+
timeout: 5_000,
|
|
520
|
+
retry: false,
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
return response.status === 200
|
|
524
|
+
} catch (error) {
|
|
525
|
+
console.error('Health check failed:', error)
|
|
526
|
+
return false
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Run health check periodically
|
|
531
|
+
setInterval(healthCheck, 60_000)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
## Common Anti-Patterns
|
|
535
|
+
|
|
536
|
+
### ❌ DON'T: Ignore Error Handling
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// Bad: Silent failures
|
|
540
|
+
try {
|
|
541
|
+
const user = await apiClient.get('/users/1')
|
|
542
|
+
return user.data
|
|
543
|
+
} catch {
|
|
544
|
+
return null // Silently fails
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Good: Explicit error handling
|
|
548
|
+
try {
|
|
549
|
+
const user = await apiClient.get('/users/1')
|
|
550
|
+
return user.data
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
553
|
+
return null
|
|
554
|
+
}
|
|
555
|
+
throw error // Re-throw unexpected errors
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### ❌ DON'T: Use Generic Error Messages
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// Bad: Generic error
|
|
563
|
+
throw new Error('Something went wrong')
|
|
564
|
+
|
|
565
|
+
// Good: Specific error with context
|
|
566
|
+
try {
|
|
567
|
+
await apiClient.get(`/users/${userId}`)
|
|
568
|
+
} catch (error) {
|
|
569
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
570
|
+
throw new Error(`Failed to fetch user ${userId}: ${message}`)
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### ❌ DON'T: Block the Event Loop
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
// Bad: Synchronous operations in middleware
|
|
578
|
+
const blockingClient = createHttpClient({
|
|
579
|
+
onRequestMiddleware: context => {
|
|
580
|
+
const token = fs.readFileSync('/path/to/token', 'utf8') // Blocks event loop
|
|
581
|
+
context.headers.set('Authorization', `Bearer ${token}`)
|
|
582
|
+
return context
|
|
583
|
+
},
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
// Good: Asynchronous operations
|
|
587
|
+
const asyncClient = createHttpClient({
|
|
588
|
+
onRequestMiddleware: async context => {
|
|
589
|
+
const token = await fs.promises.readFile('/path/to/token', 'utf8')
|
|
590
|
+
context.headers.set('Authorization', `Bearer ${token}`)
|
|
591
|
+
return context
|
|
592
|
+
},
|
|
593
|
+
})
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
## Summary
|
|
597
|
+
|
|
598
|
+
Following these best practices will help you build robust, maintainable, and performant applications with 1000fetches:
|
|
599
|
+
|
|
600
|
+
1. **Configure once, use everywhere** - Create a single, well-configured client instance
|
|
601
|
+
2. **Handle errors explicitly** - Don't let errors fail silently
|
|
602
|
+
3. **Validate data** - Use schemas to ensure data integrity
|
|
603
|
+
4. **Keep middleware lightweight** - Avoid heavy operations that slow down requests
|
|
604
|
+
5. **Test thoroughly** - Mock HTTP calls and test error scenarios
|
|
605
|
+
6. **Monitor in production** - Track request metrics and error rates
|
|
606
|
+
7. **Secure by default** - Validate input and handle sensitive data carefully
|