lambder 1.0.129 → 1.0.130

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,590 @@
1
+ # MSW Mocking for Lambder
2
+
3
+ This guide explains how to use Mock Service Worker (MSW) with Lambder to mock API endpoints in your tests and development environment.
4
+
5
+ ## Installation
6
+
7
+ MSW is an optional peer dependency. Install it in your project:
8
+
9
+ ```bash
10
+ npm install --save-dev msw
11
+ ```
12
+
13
+ ## Overview
14
+
15
+ Lambder provides type-safe mocking utilities that work seamlessly with MSW to mock your Lambder API endpoints. This is especially useful for:
16
+
17
+ - **Testing**: Write unit and integration tests without hitting real APIs
18
+ - **Development**: Develop frontend features before backend APIs are ready
19
+ - **Storybook**: Create isolated component stories with mocked data
20
+ - **Offline Development**: Work without a network connection
21
+
22
+ ## Quick Start
23
+
24
+ ### 1. Define Your API Contract
25
+
26
+ First, ensure you have your API contract defined:
27
+
28
+ ```typescript
29
+ // api-contract.ts
30
+ import { ApiContract } from 'lambder';
31
+
32
+ export const myApiContract = {
33
+ 'user.getProfile': {
34
+ input: { userId: '' as string },
35
+ output: { name: '' as string, email: '' as string }
36
+ },
37
+ 'user.updateProfile': {
38
+ input: { userId: '' as string, name: '' as string },
39
+ output: { success: true as boolean }
40
+ },
41
+ 'admin.deleteUser': {
42
+ input: { userId: '' as string },
43
+ output: { success: true as boolean }
44
+ }
45
+ } satisfies ApiContract;
46
+
47
+ export type MyApiContract = typeof myApiContract;
48
+ ```
49
+
50
+ ### 2. Set Up MSW
51
+
52
+ Create mock handlers using Lambder's mocking utilities:
53
+
54
+ ```typescript
55
+ // mocks/handlers.ts
56
+ import { mockLambderApi } from 'lambder';
57
+ import type { MyApiContract } from './api-contract';
58
+
59
+ export const handlers = [
60
+ // Basic mock with static data
61
+ mockLambderApi<MyApiContract, 'user.getProfile'>(
62
+ 'user.getProfile',
63
+ () => ({
64
+ name: 'John Doe',
65
+ email: 'john@example.com'
66
+ })
67
+ ),
68
+
69
+ // Mock with dynamic response based on input
70
+ mockLambderApi<MyApiContract, 'user.updateProfile'>(
71
+ 'user.updateProfile',
72
+ (input) => ({
73
+ success: true
74
+ }),
75
+ {
76
+ delay: 500 // Simulate network delay
77
+ }
78
+ )
79
+ ];
80
+ ```
81
+
82
+ ### 3. Initialize MSW
83
+
84
+ #### For Node.js (Tests)
85
+
86
+ ```typescript
87
+ // mocks/server.ts
88
+ import { setupServer } from 'msw/node';
89
+ import { handlers } from './handlers';
90
+
91
+ export const server = setupServer(...handlers);
92
+ ```
93
+
94
+ ```typescript
95
+ // vitest.setup.ts or jest.setup.ts
96
+ import { server } from './mocks/server';
97
+
98
+ beforeAll(() => server.listen());
99
+ afterEach(() => server.resetHandlers());
100
+ afterAll(() => server.close());
101
+ ```
102
+
103
+ #### For Browser (Development/Storybook)
104
+
105
+ ```typescript
106
+ // mocks/browser.ts
107
+ import { setupWorker } from 'msw/browser';
108
+ import { handlers } from './handlers';
109
+
110
+ export const worker = setupWorker(...handlers);
111
+ ```
112
+
113
+ ```typescript
114
+ // main.tsx or index.tsx
115
+ if (process.env.NODE_ENV === 'development') {
116
+ const { worker } = await import('./mocks/browser');
117
+ worker.start();
118
+ }
119
+ ```
120
+
121
+ ## API Reference
122
+
123
+ ### `mockLambderApi`
124
+
125
+ Creates a mock handler for a successful API response.
126
+
127
+ ```typescript
128
+ mockLambderApi<TContract, TApiName>(
129
+ apiName: TApiName,
130
+ responseFactory: (input) => output | Promise<output>,
131
+ options?: {
132
+ apiPath?: string; // Default: '/api'
133
+ delay?: number; // Delay in ms
134
+ apiVersion?: string; // Custom API version
135
+ }
136
+ )
137
+ ```
138
+
139
+ **Example:**
140
+ ```typescript
141
+ mockLambderApi<MyApiContract, 'user.getProfile'>(
142
+ 'user.getProfile',
143
+ (input) => {
144
+ // Access input with full type safety
145
+ console.log('Fetching profile for:', input.userId);
146
+
147
+ return {
148
+ name: 'John Doe',
149
+ email: 'john@example.com'
150
+ };
151
+ },
152
+ {
153
+ delay: 300, // Simulate 300ms network delay
154
+ apiPath: '/api' // Custom API path
155
+ }
156
+ )
157
+ ```
158
+
159
+ ### `mockLambderApiError`
160
+
161
+ Creates a mock handler that returns an error response.
162
+
163
+ ```typescript
164
+ mockLambderApiError<TContract, TApiName>(
165
+ apiName: TApiName,
166
+ errorMessage: string,
167
+ options?: {
168
+ apiPath?: string;
169
+ delay?: number;
170
+ apiVersion?: string;
171
+ }
172
+ )
173
+ ```
174
+
175
+ **Example:**
176
+ ```typescript
177
+ mockLambderApiError<MyApiContract, 'user.updateProfile'>(
178
+ 'user.updateProfile',
179
+ 'Failed to update profile',
180
+ { delay: 500 }
181
+ )
182
+ ```
183
+
184
+ ### `mockLambderSessionExpired`
185
+
186
+ Simulates a session expired error.
187
+
188
+ ```typescript
189
+ mockLambderSessionExpired<TContract, TApiName>(
190
+ apiName: TApiName,
191
+ options?: {
192
+ apiPath?: string;
193
+ apiVersion?: string;
194
+ }
195
+ )
196
+ ```
197
+
198
+ **Example:**
199
+ ```typescript
200
+ mockLambderSessionExpired<MyApiContract, 'user.updateProfile'>(
201
+ 'user.updateProfile'
202
+ )
203
+ ```
204
+
205
+ ### `mockLambderNotAuthorized`
206
+
207
+ Simulates a "not authorized" error.
208
+
209
+ ```typescript
210
+ mockLambderNotAuthorized<TContract, TApiName>(
211
+ apiName: TApiName,
212
+ options?: {
213
+ apiPath?: string;
214
+ apiVersion?: string;
215
+ }
216
+ )
217
+ ```
218
+
219
+ **Example:**
220
+ ```typescript
221
+ mockLambderNotAuthorized<MyApiContract, 'admin.deleteUser'>(
222
+ 'admin.deleteUser'
223
+ )
224
+ ```
225
+
226
+ ### `mockLambderVersionExpired`
227
+
228
+ Simulates a version expired error.
229
+
230
+ ```typescript
231
+ mockLambderVersionExpired<TContract, TApiName>(
232
+ apiName: TApiName,
233
+ options?: {
234
+ apiPath?: string;
235
+ apiVersion?: string;
236
+ }
237
+ )
238
+ ```
239
+
240
+ **Example:**
241
+ ```typescript
242
+ mockLambderVersionExpired<MyApiContract, 'user.getProfile'>(
243
+ 'user.getProfile'
244
+ )
245
+ ```
246
+
247
+ ### `createLambderApiHandler`
248
+
249
+ Helper function to group multiple handlers for the same API path.
250
+
251
+ ```typescript
252
+ createLambderApiHandler(
253
+ apiPath: string,
254
+ handlers: HttpHandler[]
255
+ )
256
+ ```
257
+
258
+ **Example:**
259
+ ```typescript
260
+ import { setupServer } from 'msw/node';
261
+ import { createLambderApiHandler, mockLambderApi } from 'lambder';
262
+
263
+ const server = setupServer(
264
+ createLambderApiHandler('/api', [
265
+ mockLambderApi<MyApiContract, 'user.getProfile'>('user.getProfile', () => ({
266
+ name: 'Test User',
267
+ email: 'test@example.com'
268
+ })),
269
+ mockLambderApi<MyApiContract, 'user.updateProfile'>('user.updateProfile', (input) => ({
270
+ success: true
271
+ }))
272
+ ])
273
+ );
274
+ ```
275
+
276
+ ## Testing Examples
277
+
278
+ ### Vitest
279
+
280
+ ```typescript
281
+ // user-profile.test.ts
282
+ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
283
+ import { server } from './mocks/server';
284
+ import { mockLambderApi } from 'lambder';
285
+ import type { MyApiContract } from './api-contract';
286
+
287
+ describe('User Profile', () => {
288
+ beforeAll(() => server.listen());
289
+ afterEach(() => server.resetHandlers());
290
+ afterAll(() => server.close());
291
+
292
+ it('should fetch user profile', async () => {
293
+ // Test with mocked data
294
+ const result = await caller.api('user.getProfile', { userId: '123' });
295
+
296
+ expect(result).toEqual({
297
+ name: 'John Doe',
298
+ email: 'john@example.com'
299
+ });
300
+ });
301
+
302
+ it('should handle errors', async () => {
303
+ // Override handler for this test
304
+ server.use(
305
+ mockLambderApiError<MyApiContract, 'user.getProfile'>(
306
+ 'user.getProfile',
307
+ 'User not found'
308
+ )
309
+ );
310
+
311
+ const result = await caller.api('user.getProfile', { userId: '999' });
312
+
313
+ expect(result).toBeNull();
314
+ });
315
+
316
+ it('should handle session expiry', async () => {
317
+ server.use(
318
+ mockLambderSessionExpired<MyApiContract, 'user.updateProfile'>(
319
+ 'user.updateProfile'
320
+ )
321
+ );
322
+
323
+ const result = await caller.api('user.updateProfile', {
324
+ userId: '123',
325
+ name: 'New Name'
326
+ });
327
+
328
+ expect(result).toBeNull();
329
+ });
330
+ });
331
+ ```
332
+
333
+ ### React Testing Library
334
+
335
+ ```typescript
336
+ import { render, screen, waitFor } from '@testing-library/react';
337
+ import userEvent from '@testing-library/user-event';
338
+ import { server } from './mocks/server';
339
+ import { mockLambderApi } from 'lambder';
340
+ import UserProfile from './UserProfile';
341
+
342
+ test('displays user profile', async () => {
343
+ render(<UserProfile userId="123" />);
344
+
345
+ await waitFor(() => {
346
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
347
+ expect(screen.getByText('john@example.com')).toBeInTheDocument();
348
+ });
349
+ });
350
+
351
+ test('handles update', async () => {
352
+ const user = userEvent.setup();
353
+ render(<UserProfile userId="123" />);
354
+
355
+ const nameInput = screen.getByLabelText('Name');
356
+ await user.clear(nameInput);
357
+ await user.type(nameInput, 'Jane Doe');
358
+
359
+ await user.click(screen.getByText('Save'));
360
+
361
+ await waitFor(() => {
362
+ expect(screen.getByText('Profile updated successfully')).toBeInTheDocument();
363
+ });
364
+ });
365
+ ```
366
+
367
+ ## Advanced Usage
368
+
369
+ ### Dynamic Responses Based on Input
370
+
371
+ ```typescript
372
+ mockLambderApi<MyApiContract, 'user.getProfile'>(
373
+ 'user.getProfile',
374
+ (input) => {
375
+ // Return different data based on input
376
+ const profiles = {
377
+ '1': { name: 'John Doe', email: 'john@example.com' },
378
+ '2': { name: 'Jane Smith', email: 'jane@example.com' }
379
+ };
380
+
381
+ return profiles[input.userId] || { name: 'Unknown', email: '' };
382
+ }
383
+ )
384
+ ```
385
+
386
+ ### Simulating Loading States
387
+
388
+ ```typescript
389
+ mockLambderApi<MyApiContract, 'user.getProfile'>(
390
+ 'user.getProfile',
391
+ async (input) => {
392
+ // Simulate slow network
393
+ await new Promise(resolve => setTimeout(resolve, 2000));
394
+
395
+ return {
396
+ name: 'John Doe',
397
+ email: 'john@example.com'
398
+ };
399
+ },
400
+ {
401
+ delay: 2000 // Alternative way to add delay
402
+ }
403
+ )
404
+ ```
405
+
406
+ ### Conditional Error Responses
407
+
408
+ ```typescript
409
+ mockLambderApi<MyApiContract, 'user.updateProfile'>(
410
+ 'user.updateProfile',
411
+ (input) => {
412
+ // Validate input and throw if needed
413
+ if (!input.name || input.name.length < 2) {
414
+ throw new Error('Name must be at least 2 characters');
415
+ }
416
+
417
+ return { success: true };
418
+ }
419
+ )
420
+ ```
421
+
422
+ ### Multiple API Paths
423
+
424
+ ```typescript
425
+ // For different environments or API versions
426
+ export const devHandlers = [
427
+ mockLambderApi<MyApiContract, 'user.getProfile'>(
428
+ 'user.getProfile',
429
+ () => ({ name: 'Dev User', email: 'dev@example.com' }),
430
+ { apiPath: '/dev/api' }
431
+ )
432
+ ];
433
+
434
+ export const prodHandlers = [
435
+ mockLambderApi<MyApiContract, 'user.getProfile'>(
436
+ 'user.getProfile',
437
+ () => ({ name: 'Prod User', email: 'prod@example.com' }),
438
+ { apiPath: '/api' }
439
+ )
440
+ ];
441
+ ```
442
+
443
+ ## Storybook Integration
444
+
445
+ ```typescript
446
+ // .storybook/preview.tsx
447
+ import { initialize, mswLoader } from 'msw-storybook-addon';
448
+ import { handlers } from '../mocks/handlers';
449
+
450
+ initialize();
451
+
452
+ export const loaders = [mswLoader];
453
+
454
+ export const parameters = {
455
+ msw: {
456
+ handlers
457
+ }
458
+ };
459
+ ```
460
+
461
+ ```typescript
462
+ // UserProfile.stories.tsx
463
+ import type { Meta, StoryObj } from '@storybook/react';
464
+ import { mockLambderApi, mockLambderApiError } from 'lambder';
465
+ import type { MyApiContract } from './api-contract';
466
+ import UserProfile from './UserProfile';
467
+
468
+ const meta: Meta<typeof UserProfile> = {
469
+ component: UserProfile,
470
+ };
471
+
472
+ export default meta;
473
+ type Story = StoryObj<typeof UserProfile>;
474
+
475
+ export const Default: Story = {
476
+ args: {
477
+ userId: '123'
478
+ },
479
+ parameters: {
480
+ msw: {
481
+ handlers: [
482
+ mockLambderApi<MyApiContract, 'user.getProfile'>(
483
+ 'user.getProfile',
484
+ () => ({
485
+ name: 'John Doe',
486
+ email: 'john@example.com'
487
+ })
488
+ )
489
+ ]
490
+ }
491
+ }
492
+ };
493
+
494
+ export const LoadingState: Story = {
495
+ args: {
496
+ userId: '123'
497
+ },
498
+ parameters: {
499
+ msw: {
500
+ handlers: [
501
+ mockLambderApi<MyApiContract, 'user.getProfile'>(
502
+ 'user.getProfile',
503
+ () => ({
504
+ name: 'John Doe',
505
+ email: 'john@example.com'
506
+ }),
507
+ { delay: 3000 }
508
+ )
509
+ ]
510
+ }
511
+ }
512
+ };
513
+
514
+ export const ErrorState: Story = {
515
+ args: {
516
+ userId: '123'
517
+ },
518
+ parameters: {
519
+ msw: {
520
+ handlers: [
521
+ mockLambderApiError<MyApiContract, 'user.getProfile'>(
522
+ 'user.getProfile',
523
+ 'Failed to load user profile'
524
+ )
525
+ ]
526
+ }
527
+ }
528
+ };
529
+ ```
530
+
531
+ ## Best Practices
532
+
533
+ 1. **Keep Handlers in a Separate Directory**: Organize your mocks in a `mocks/` folder for easy maintenance.
534
+
535
+ 2. **Use Type Safety**: Always specify your API contract types to get full IntelliSense support.
536
+
537
+ 3. **Reset Handlers Between Tests**: Use `server.resetHandlers()` in `afterEach` to prevent test pollution.
538
+
539
+ 4. **Mock Realistic Data**: Use realistic data in your mocks to catch potential issues early.
540
+
541
+ 5. **Test Error Cases**: Don't just test happy paths - use error mocks to test error handling.
542
+
543
+ 6. **Use Delays Sparingly**: Only add delays when testing loading states to keep tests fast.
544
+
545
+ 7. **Version Your Mocks**: If using API versions, include them in your mock setup.
546
+
547
+ ## Troubleshooting
548
+
549
+ ### MSW Not Working
550
+
551
+ If MSW handlers aren't being called:
552
+
553
+ 1. Ensure MSW is properly initialized before your tests/app
554
+ 2. Check that the API path matches your configuration
555
+ 3. Verify the API name in the handler matches what you're calling
556
+ 4. Check browser console for MSW logs
557
+
558
+ ### TypeScript Errors
559
+
560
+ If you see TypeScript errors about missing MSW types:
561
+
562
+ ```bash
563
+ npm install --save-dev @types/msw
564
+ ```
565
+
566
+ Or use the built-in `HttpHandler` type from Lambder:
567
+
568
+ ```typescript
569
+ import type { HttpHandler } from 'lambder';
570
+ ```
571
+
572
+ ### Handlers Not Matching
573
+
574
+ MSW handlers are checked in order. If a handler isn't matching:
575
+
576
+ 1. Put more specific handlers before generic ones
577
+ 2. Use `server.listHandlers()` to debug which handlers are registered
578
+ 3. Add console.log statements in your response factories
579
+
580
+ ## Resources
581
+
582
+ - [MSW Documentation](https://mswjs.io/)
583
+ - [MSW Storybook Addon](https://storybook.js.org/addons/msw-storybook-addon)
584
+ - [Lambder API Contract Documentation](./TYPE_SAFE_QUICK_START.md)
585
+
586
+ ## Support
587
+
588
+ For issues or questions:
589
+ - Open an issue on [GitHub](https://github.com/nesovera/lambder)
590
+ - Check existing documentation in the `/docs` folder