lambder 1.0.129 → 1.0.131

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