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.
@@ -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