lambder 1.0.128 → 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
@@ -26,11 +26,13 @@ const lambder = new Lambder<MyApiContract>({
26
26
  apiPath: '/api'
27
27
  });
28
28
 
29
- // Now addApi is type-safe!
29
+ // Now addApi is type-safe for both inputs AND outputs!
30
30
  lambder.addApi('getUserById', async (ctx, resolver) => {
31
- // ctx.apiPayload is automatically typed as { userId: string }
31
+ // ✅ ctx.apiPayload is automatically typed as { userId: string }
32
32
  const user = await db.getUser(ctx.apiPayload.userId);
33
- return resolver.api(user);
33
+
34
+ // ✅ resolver.api() enforces the output type (User)
35
+ return resolver.api(user); // TypeScript checks that user matches User type!
34
36
  });
35
37
  ```
36
38
 
@@ -55,7 +57,8 @@ const user = await caller.api('getUserById', { userId: '123' });
55
57
  - ✅ **No wrapper functions needed**
56
58
  - ✅ **Use existing `api()` and `addApi()` methods**
57
59
  - ✅ **Full autocomplete in IDE**
58
- - ✅ **Type-safe inputs and outputs**
60
+ - ✅ **Type-safe inputs AND outputs**
61
+ - ✅ **Compile-time validation**
59
62
  - ✅ **Backward compatible**
60
63
 
61
64
  ## Contract Type Format
@@ -94,6 +97,45 @@ login: {
94
97
  }
95
98
  ```
96
99
 
100
+ ## Output Type Enforcement
101
+
102
+ The type system now enforces that `resolver.api()` returns data matching your contract's output type:
103
+
104
+ ```typescript
105
+ type MyContract = ApiContract<{
106
+ getNumber: { input: void, output: number },
107
+ getUser: { input: { id: string }, output: User }
108
+ }>;
109
+
110
+ const lambder = new Lambder<MyContract>({ ... });
111
+
112
+ // ✅ CORRECT
113
+ lambder.addApi('getNumber', async (ctx, resolver) => {
114
+ return resolver.api(42); // number - matches output type
115
+ });
116
+
117
+ // ❌ ERROR: Type 'string' is not assignable to type 'number'
118
+ lambder.addApi('getNumber', async (ctx, resolver) => {
119
+ return resolver.api("wrong"); // TypeScript error!
120
+ });
121
+
122
+ // ✅ CORRECT
123
+ lambder.addApi('getUser', async (ctx, resolver) => {
124
+ return resolver.api({ id: "123", name: "John", ... }); // User object
125
+ });
126
+
127
+ // ❌ ERROR: Missing required properties
128
+ lambder.addApi('getUser', async (ctx, resolver) => {
129
+ return resolver.api({ id: "123" }); // TypeScript error - incomplete User!
130
+ });
131
+ ```
132
+
133
+ This works with:
134
+ - `resolver.api()` - typed return value
135
+ - `resolver.die.api()` - typed return value
136
+ - `addApi()` - typed for regular APIs
137
+ - `addSessionApi()` - typed for session APIs
138
+
97
139
  ## Without Type Safety (Still Works!)
98
140
 
99
141
  Don't want type safety? Just don't pass the generic type:
@@ -118,6 +160,8 @@ See [simplified-typed-api-example.ts](../examples/simplified-typed-api-example.t
118
160
 
119
161
  - **Contract is just a TypeScript type** - No runtime code!
120
162
  - **Zero overhead** - All type checking happens at compile time
163
+ - **Input AND output validation** - Both sides of your API are type-safe
164
+ - **Compile-time safety** - Catch type mismatches before deployment
121
165
  - **Opt-in** - Use types when you want them
122
166
  - **Simple** - Just pass type to constructor
123
167
  - **Autocomplete** - IDE shows available APIs as you type