lambder 1.0.125 → 1.0.126

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/Readme.md CHANGED
@@ -485,6 +485,65 @@ const loadPageData = async () => {
485
485
  };
486
486
  ```
487
487
 
488
+ ## Type-Safe APIs (Optional)
489
+
490
+ Want compile-time type checking for your APIs? It's incredibly simple!
491
+
492
+ ### 1. Define Your API Contract
493
+
494
+ ```typescript
495
+ // shared/apiContract.ts
496
+ import type { ApiContract } from 'lambder';
497
+
498
+ export type MyApiContract = {
499
+ getUserById: { input: { userId: string }, output: User },
500
+ createUser: { input: CreateUserInput, output: User },
501
+ listUsers: { input: void, output: User[] }
502
+ } satisfies ApiContract;
503
+ ```
504
+
505
+ ### 2. Backend - Pass Type to Constructor
506
+
507
+ ```typescript
508
+ import Lambder from 'lambder';
509
+ import type { MyApiContract } from './shared/apiContract';
510
+
511
+ const lambder = new Lambder<MyApiContract>({ publicPath: './public', apiPath: '/api' });
512
+
513
+ // Now addApi is type-safe!
514
+ lambder.addApi('getUserById', async (ctx, resolver) => {
515
+ // ctx.apiPayload is automatically typed as { userId: string } ✨
516
+ const user = await db.getUser(ctx.apiPayload.userId);
517
+ return resolver.api(user);
518
+ });
519
+ ```
520
+
521
+ ### 3. Frontend - Pass Type to Constructor
522
+
523
+ ```typescript
524
+ import { LambderCaller } from 'lambder';
525
+ import type { MyApiContract } from './shared/apiContract';
526
+
527
+ const caller = new LambderCaller<MyApiContract>({ apiPath: '/api', isCorsEnabled: false });
528
+
529
+ // Now api() is type-safe with full autocomplete! ✨
530
+ const user = await caller.api('getUserById', { userId: '123' });
531
+ // ↑ IDE shows all available APIs
532
+ // ↑ Type-checked input
533
+ // user is typed as User | null | undefined
534
+ ```
535
+
536
+ ### Benefits
537
+
538
+ ✅ **Simple** - Just pass type to constructor, that's it!
539
+ ✅ **Autocomplete** - IDE suggests available APIs as you type
540
+ ✅ **Type Safety** - Inputs and outputs are fully typed
541
+ ✅ **No Wrappers** - Use existing `api()` and `addApi()` methods
542
+ ✅ **Opt-In** - Add when you want, skip when you don't
543
+ ✅ **Zero Overhead** - Pure TypeScript types, no runtime code
544
+
545
+ 📖 **[Read the Quick Start Guide](docs/TYPE_SAFE_QUICK_START.md)** for more details and examples!
546
+
488
547
  ## Contributing
489
548
 
490
549
  Contributions are welcome! Especially for documentation. If you have an idea for an improvement or have found a bug, please open an issue or submit a pull request.
package/dist/Lambder.d.ts CHANGED
@@ -4,6 +4,7 @@ import LambderResponseBuilder, { LambderResolverResponse } from "./LambderRespon
4
4
  import LambderUtils from "./LambderUtils.js";
5
5
  import { type LambderSessionContext } from "./LambderSessionManager.js";
6
6
  import LambderSessionController from "./LambderSessionController.js";
7
+ import type { ApiContract } from "./LambderApiContract.js";
7
8
  type Path = `/${string}`;
8
9
  export type LambderRenderContext = {
9
10
  host: string;
@@ -43,7 +44,7 @@ type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext | null,
43
44
  type RouteFallbackHandlerFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
44
45
  type ApiFallbackHandlerFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
45
46
  export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext;
46
- export default class Lambder {
47
+ export default class Lambder<TContract extends ApiContract = any> {
47
48
  apiPath: string;
48
49
  apiVersion: null | string;
49
50
  isCorsEnabled: boolean;
@@ -86,8 +87,16 @@ export default class Lambder {
86
87
  }>): Promise<void>;
87
88
  addRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
88
89
  addSessionRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
89
- addApi(apiName: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
90
- addSessionApi(apiName: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
90
+ addApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
91
+ addApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext & {
92
+ apiPayload: TContract[TApiName]['input'];
93
+ }, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
94
+ addApi(apiName: string, actionFn: ActionFunction): void;
95
+ addSessionApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
96
+ addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext & {
97
+ apiPayload: TContract[TApiName]['input'];
98
+ }, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
99
+ addSessionApi(apiName: string, actionFn: ActionFunction): void;
91
100
  addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
92
101
  addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
93
102
  addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
package/dist/Lambder.js CHANGED
@@ -163,6 +163,7 @@ export default class Lambder {
163
163
  });
164
164
  }
165
165
  ;
166
+ // Implementation
166
167
  addApi(apiName, actionFn) {
167
168
  this.actionList.push({
168
169
  conditionFn: (ctx) => (!!ctx.apiName && ((typeof apiName === "string" && ctx.apiName === apiName) ||
@@ -172,6 +173,7 @@ export default class Lambder {
172
173
  });
173
174
  }
174
175
  ;
176
+ // Implementation
175
177
  addSessionApi(apiName, actionFn) {
176
178
  this.actionList.push({
177
179
  conditionFn: (ctx) => (!!ctx.apiName && ((typeof apiName === "string" && ctx.apiName === apiName) ||
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Type-safe API Contract System
3
+ *
4
+ * Define your API contract as a TypeScript type to get full type safety
5
+ * across frontend and backend with no runtime overhead.
6
+ *
7
+ * Example:
8
+ *
9
+ * export type MyApiContract = {
10
+ * getUserById: { input: { userId: string }, output: User },
11
+ * createUser: { input: CreateUserInput, output: User },
12
+ * listUsers: { input: void, output: User[] }
13
+ * }
14
+ *
15
+ * Frontend:
16
+ * const caller = new LambderCaller<MyApiContract>({ ... });
17
+ * const user = await caller.api('getUserById', { userId: '123' }); // typed!
18
+ *
19
+ * Backend:
20
+ * const lambder = new Lambder<MyApiContract>({ ... });
21
+ * lambder.addApi('getUserById', async (ctx, resolver) => {
22
+ * // ctx.apiPayload is typed as { userId: string }
23
+ * return resolver.api(user); // user is typed as User
24
+ * });
25
+ */
26
+ /**
27
+ * Base type for API contracts
28
+ */
29
+ export type ApiContract = {
30
+ [apiName: string]: {
31
+ input: any;
32
+ output: any;
33
+ };
34
+ };
35
+ /**
36
+ * Extract input type from contract for a specific API
37
+ */
38
+ export type ApiInput<TContract extends ApiContract, TApiName extends keyof TContract> = TContract[TApiName]['input'];
39
+ /**
40
+ * Extract output type from contract for a specific API
41
+ */
42
+ export type ApiOutput<TContract extends ApiContract, TApiName extends keyof TContract> = TContract[TApiName]['output'];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Type-safe API Contract System
3
+ *
4
+ * Define your API contract as a TypeScript type to get full type safety
5
+ * across frontend and backend with no runtime overhead.
6
+ *
7
+ * Example:
8
+ *
9
+ * export type MyApiContract = {
10
+ * getUserById: { input: { userId: string }, output: User },
11
+ * createUser: { input: CreateUserInput, output: User },
12
+ * listUsers: { input: void, output: User[] }
13
+ * }
14
+ *
15
+ * Frontend:
16
+ * const caller = new LambderCaller<MyApiContract>({ ... });
17
+ * const user = await caller.api('getUserById', { userId: '123' }); // typed!
18
+ *
19
+ * Backend:
20
+ * const lambder = new Lambder<MyApiContract>({ ... });
21
+ * lambder.addApi('getUserById', async (ctx, resolver) => {
22
+ * // ctx.apiPayload is typed as { userId: string }
23
+ * return resolver.api(user); // user is typed as User
24
+ * });
25
+ */
26
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { LambderApiResponse } from './LambderResponseBuilder';
2
+ import type { ApiContract } from './LambderApiContract';
2
3
  type VoidFunction = () => void | Promise<void>;
3
4
  type FetchTracker = {
4
5
  apiName: string;
@@ -21,7 +22,7 @@ type FetchEndEventHandler = (params: {
21
22
  }) => void | Promise<void>;
22
23
  type ErrorHandler = (err: Error) => void | Promise<void>;
23
24
  type MessageHandler = (message: any) => void | Promise<void>;
24
- export default class LambderCaller {
25
+ export default class LambderCaller<TContract extends ApiContract = any> {
25
26
  private isCorsEnabled;
26
27
  private apiPath;
27
28
  private apiVersion?;
@@ -51,7 +52,7 @@ export default class LambderCaller {
51
52
  fetchEndedHandler?: FetchEndEventHandler;
52
53
  });
53
54
  setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): void;
54
- apiRaw<T = any>(apiName: string, payload?: any, options?: {
55
+ apiRaw<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, options?: {
55
56
  headers?: Record<string, any>;
56
57
  versionExpiredHandler?: VoidFunction;
57
58
  sessionExpiredHandler?: VoidFunction;
@@ -61,7 +62,17 @@ export default class LambderCaller {
61
62
  errorHandler?: ErrorHandler;
62
63
  fetchStartedHandler?: FetchStartEventHandler;
63
64
  fetchEndedHandler?: FetchEndEventHandler;
64
- }): Promise<LambderApiResponse<T> | null | undefined>;
65
- api<T = any>(...params: Parameters<typeof LambderCaller.prototype.apiRaw>): Promise<T | null | undefined>;
65
+ }): Promise<LambderApiResponse<TOutput> | null | undefined>;
66
+ api<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, options?: {
67
+ headers?: Record<string, any>;
68
+ versionExpiredHandler?: VoidFunction;
69
+ sessionExpiredHandler?: VoidFunction;
70
+ messageHandler?: MessageHandler;
71
+ errorMessageHandler?: MessageHandler;
72
+ notAuthorizedHandler?: VoidFunction;
73
+ errorHandler?: ErrorHandler;
74
+ fetchStartedHandler?: FetchStartEventHandler;
75
+ fetchEndedHandler?: FetchEndEventHandler;
76
+ }): Promise<TOutput | null | undefined>;
66
77
  }
67
78
  export {};
@@ -131,8 +131,8 @@ export default class LambderCaller {
131
131
  }
132
132
  ;
133
133
  // Use the same type for api but adjust the return type
134
- async api(...params) {
135
- const result = await this.apiRaw(...params);
134
+ async api(apiName, payload, options) {
135
+ const result = await this.apiRaw(apiName, payload, options);
136
136
  return result?.payload;
137
137
  }
138
138
  }
package/dist/index.d.ts CHANGED
@@ -4,3 +4,4 @@ export { default as LambderCaller } from "./LambderCaller.js";
4
4
  export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
5
5
  export { default as LambderResolver } from "./LambderResolver.js";
6
6
  export { default as LambderSessionManager } from "./LambderSessionManager.js";
7
+ export { type ApiContract, type ApiInput, type ApiOutput, } from "./LambderApiContract.js";
@@ -0,0 +1,123 @@
1
+ # Type-Safe API Quick Start
2
+
3
+ ## In 3 Simple Steps
4
+
5
+ ### 1. Define Your Contract Type
6
+
7
+ ```typescript
8
+ // shared/apiContract.ts
9
+ import type { ApiContract } from 'lambder';
10
+
11
+ export type MyApiContract = {
12
+ getUserById: { input: { userId: string }, output: User },
13
+ createUser: { input: CreateUserInput, output: User },
14
+ listUsers: { input: void, output: User[] }
15
+ } satisfies ApiContract;
16
+ ```
17
+
18
+ ### 2. Backend - Pass Type to Lambder
19
+
20
+ ```typescript
21
+ import Lambder from 'lambder';
22
+ import type { MyApiContract } from './shared/apiContract';
23
+
24
+ const lambder = new Lambder<MyApiContract>({
25
+ publicPath: './public',
26
+ apiPath: '/api'
27
+ });
28
+
29
+ // Now addApi is type-safe!
30
+ lambder.addApi('getUserById', async (ctx, resolver) => {
31
+ // ctx.apiPayload is automatically typed as { userId: string }
32
+ const user = await db.getUser(ctx.apiPayload.userId);
33
+ return resolver.api(user);
34
+ });
35
+ ```
36
+
37
+ ### 3. Frontend - Pass Type to LambderCaller
38
+
39
+ ```typescript
40
+ import { LambderCaller } from 'lambder';
41
+ import type { MyApiContract } from './shared/apiContract';
42
+
43
+ const caller = new LambderCaller<MyApiContract>({
44
+ apiPath: '/api',
45
+ isCorsEnabled: false
46
+ });
47
+
48
+ // Now api() is type-safe!
49
+ const user = await caller.api('getUserById', { userId: '123' });
50
+ // user is typed as User | null | undefined
51
+ ```
52
+
53
+ ## That's It!
54
+
55
+ - ✅ **No wrapper functions needed**
56
+ - ✅ **Use existing `api()` and `addApi()` methods**
57
+ - ✅ **Full autocomplete in IDE**
58
+ - ✅ **Type-safe inputs and outputs**
59
+ - ✅ **Backward compatible**
60
+
61
+ ## Contract Type Format
62
+
63
+ ```typescript
64
+ type MyApiContract = {
65
+ apiName: { input: InputType, output: OutputType }
66
+ }
67
+ ```
68
+
69
+ ## Examples
70
+
71
+ ### API with parameters
72
+ ```typescript
73
+ getUserById: { input: { userId: string }, output: User }
74
+ ```
75
+
76
+ ### API with no input
77
+ ```typescript
78
+ listAll: { input: void, output: Item[] }
79
+ ```
80
+
81
+ ### API with complex types
82
+ ```typescript
83
+ updateUser: {
84
+ input: { id: string } & Partial<User>,
85
+ output: User
86
+ }
87
+ ```
88
+
89
+ ### API with conditional output
90
+ ```typescript
91
+ login: {
92
+ input: { email: string, password: string },
93
+ output: { success: boolean, user?: User, error?: string }
94
+ }
95
+ ```
96
+
97
+ ## Without Type Safety (Still Works!)
98
+
99
+ Don't want type safety? Just don't pass the generic type:
100
+
101
+ ```typescript
102
+ // Frontend
103
+ const caller = new LambderCaller({ ... }); // No generic
104
+ await caller.api('anyApi', { anything: true }); // Works, but untyped
105
+
106
+ // Backend
107
+ const lambder = new Lambder({ ... }); // No generic
108
+ lambder.addApi('anyApi', async (ctx, resolver) => {
109
+ // ctx.apiPayload is any
110
+ });
111
+ ```
112
+
113
+ ## Full Example
114
+
115
+ See [simplified-typed-api-example.ts](../examples/simplified-typed-api-example.ts) for a complete working example.
116
+
117
+ ## Key Points
118
+
119
+ - **Contract is just a TypeScript type** - No runtime code!
120
+ - **Zero overhead** - All type checking happens at compile time
121
+ - **Opt-in** - Use types when you want them
122
+ - **Simple** - Just pass type to constructor
123
+ - **Autocomplete** - IDE shows available APIs as you type
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Simplified Type-Safe API Example
3
+ *
4
+ * This example shows how to use Lambder's opt-in type-safe API system.
5
+ * Simply pass your API contract type to LambderCaller and Lambder constructors,
6
+ * and get full type safety with no extra wrapper functions needed!
7
+ */
8
+
9
+ import Lambder from '../src/Lambder.js';
10
+ import LambderCaller from '../src/LambderCaller.js';
11
+ import type { ApiContract } from '../src/index.js';
12
+
13
+ // ============================================================================
14
+ // Step 1: Define your data types
15
+ // ============================================================================
16
+
17
+ type User = {
18
+ id: string;
19
+ name: string;
20
+ email: string;
21
+ role: 'admin' | 'user';
22
+ createdAt: string;
23
+ };
24
+
25
+ type CreateUserInput = {
26
+ name: string;
27
+ email: string;
28
+ password: string;
29
+ };
30
+
31
+ type UpdateUserInput = {
32
+ userId: string;
33
+ name?: string;
34
+ email?: string;
35
+ };
36
+
37
+ type LoginInput = {
38
+ email: string;
39
+ password: string;
40
+ };
41
+
42
+ type LoginOutput = {
43
+ success: boolean;
44
+ user?: User;
45
+ token?: string;
46
+ error?: string;
47
+ };
48
+
49
+ // ============================================================================
50
+ // Step 2: Define your API contract (shared between frontend and backend)
51
+ // ============================================================================
52
+
53
+ export type MyApiContract = {
54
+ // API with input and output
55
+ getUserById: { input: { userId: string }, output: User },
56
+
57
+ // API with complex input/output
58
+ createUser: { input: CreateUserInput, output: User },
59
+ updateUser: { input: UpdateUserInput, output: User },
60
+
61
+ // API with void input (no parameters needed)
62
+ listUsers: { input: void, output: User[] },
63
+ getCurrentUser: { input: void, output: User },
64
+
65
+ // API with conditional output
66
+ login: { input: LoginInput, output: LoginOutput },
67
+
68
+ // API with primitive output
69
+ getUserCount: { input: void, output: number },
70
+ deleteUser: { input: { userId: string }, output: boolean },
71
+ }
72
+
73
+ // ============================================================================
74
+ // Step 3: Backend - Pass contract type to Lambder
75
+ // ============================================================================
76
+
77
+ export function setupBackend() {
78
+ // Pass the contract type as a generic parameter
79
+ const lambder = new Lambder<MyApiContract>({
80
+ publicPath: './public',
81
+ apiPath: '/api',
82
+ apiVersion: '1.0.0',
83
+ });
84
+
85
+ // Now addApi is type-safe! ctx.apiPayload is automatically typed!
86
+ lambder.addApi('getUserById', async (ctx, resolver) => {
87
+ // ctx.apiPayload is typed as { userId: string }
88
+ const userId = ctx.apiPayload.userId; // ✅ TypeScript knows this!
89
+
90
+ // Mock database call
91
+ const user: User = {
92
+ id: userId,
93
+ name: 'John Doe',
94
+ email: 'john@example.com',
95
+ role: 'user',
96
+ createdAt: new Date().toISOString(),
97
+ };
98
+
99
+ return resolver.api(user);
100
+ });
101
+
102
+ // Session API with typed payload
103
+ lambder.addSessionApi('createUser', async (ctx, resolver) => {
104
+ // ctx.apiPayload is typed as CreateUserInput
105
+ const { name, email, password } = ctx.apiPayload;
106
+
107
+ // Validation with type safety
108
+ if (!name || !email || !password) {
109
+ return resolver.api(null, {
110
+ errorMessage: 'Missing required fields'
111
+ });
112
+ }
113
+
114
+ // Create user
115
+ const newUser: User = {
116
+ id: Math.random().toString(36).substr(2, 9),
117
+ name,
118
+ email,
119
+ role: 'user',
120
+ createdAt: new Date().toISOString(),
121
+ };
122
+
123
+ return resolver.api(newUser);
124
+ });
125
+
126
+ // API with void input
127
+ lambder.addApi('listUsers', async (ctx, resolver) => {
128
+ // ctx.apiPayload is void/undefined
129
+ const users: User[] = [
130
+ { id: '1', name: 'John', email: 'john@example.com', role: 'user', createdAt: new Date().toISOString() },
131
+ { id: '2', name: 'Jane', email: 'jane@example.com', role: 'admin', createdAt: new Date().toISOString() },
132
+ ];
133
+
134
+ return resolver.api(users);
135
+ });
136
+
137
+ // Complex API with conditional response
138
+ lambder.addApi('login', async (ctx, resolver) => {
139
+ // ctx.apiPayload is typed as LoginInput
140
+ const { email, password } = ctx.apiPayload;
141
+
142
+ // Mock authentication
143
+ if (email === 'test@example.com' && password === 'password123') {
144
+ const result: LoginOutput = {
145
+ success: true,
146
+ user: {
147
+ id: '1',
148
+ name: 'Test User',
149
+ email: email,
150
+ role: 'user',
151
+ createdAt: new Date().toISOString(),
152
+ },
153
+ token: 'mock-jwt-token',
154
+ };
155
+ return resolver.api(result);
156
+ } else {
157
+ const result: LoginOutput = {
158
+ success: false,
159
+ error: 'Invalid credentials',
160
+ };
161
+ return resolver.api(result);
162
+ }
163
+ });
164
+
165
+ // You can still use RegExp or functions for dynamic patterns (untyped)
166
+ lambder.addApi(/^admin\./, async (ctx, resolver) => {
167
+ // ctx.apiPayload is any (untyped)
168
+ return resolver.api({ message: 'Admin API' });
169
+ });
170
+
171
+ return lambder;
172
+ }
173
+
174
+ // ============================================================================
175
+ // Step 4: Frontend - Pass contract type to LambderCaller
176
+ // ============================================================================
177
+
178
+ export function setupFrontend() {
179
+ // Pass the contract type as a generic parameter
180
+ const caller = new LambderCaller<MyApiContract>({
181
+ apiPath: '/api',
182
+ apiVersion: '1.0.0',
183
+ isCorsEnabled: false,
184
+ errorHandler: (err) => {
185
+ console.error('API Error:', err);
186
+ },
187
+ });
188
+
189
+ // Now all API calls are type-safe!
190
+
191
+ // Example 1: Get user by ID
192
+ async function example1() {
193
+ // TypeScript knows:
194
+ // - First parameter is 'getUserById' (autocomplete shows all API names!)
195
+ // - Second parameter must be { userId: string }
196
+ // - Return type is User | null | undefined
197
+ const user = await caller.api('getUserById', { userId: '123' });
198
+
199
+ if (user) {
200
+ console.log(user.name); // ✅ TypeScript knows 'name' exists
201
+ console.log(user.email); // ✅ TypeScript knows 'email' exists
202
+ console.log(user.role); // ✅ TypeScript knows 'role' is 'admin' | 'user'
203
+ // console.log(user.age); // ✗ Error: Property 'age' does not exist
204
+ }
205
+ }
206
+
207
+ // Example 2: Create user
208
+ async function example2() {
209
+ // TypeScript enforces the CreateUserInput type
210
+ const newUser = await caller.api('createUser', {
211
+ name: 'Alice',
212
+ email: 'alice@example.com',
213
+ password: 'secret123',
214
+ });
215
+
216
+ if (newUser) {
217
+ console.log('Created user:', newUser.id);
218
+ }
219
+
220
+ // This would be a TypeScript error:
221
+ // await caller.api('createUser', { name: 'Bob' }); // ✗ Missing email and password
222
+ }
223
+
224
+ // Example 3: Login
225
+ async function example3() {
226
+ const result = await caller.api('login', {
227
+ email: 'test@example.com',
228
+ password: 'password123',
229
+ });
230
+
231
+ if (result?.success && result.user) {
232
+ console.log('Logged in as:', result.user.name);
233
+ console.log('Token:', result.token);
234
+ } else {
235
+ console.error('Login failed:', result?.error);
236
+ }
237
+ }
238
+
239
+ // Example 4: List users (void input)
240
+ async function example4() {
241
+ // For void input, pass undefined
242
+ const users = await caller.api('listUsers', undefined);
243
+
244
+ if (users) {
245
+ users.forEach(user => {
246
+ console.log(user.name); // ✅ TypeScript knows the array type
247
+ });
248
+ }
249
+ }
250
+
251
+ // Example 5: Get user count (primitive output)
252
+ async function example5() {
253
+ const count = await caller.api('getUserCount', undefined);
254
+ if (count !== null && count !== undefined) {
255
+ console.log(`Total users: ${count}`); // count is number
256
+ }
257
+ }
258
+
259
+ // Example 6: With custom headers
260
+ async function example6() {
261
+ const user = await caller.api(
262
+ 'getUserById',
263
+ { userId: '456' },
264
+ { headers: { 'X-Custom-Header': 'value' } }
265
+ );
266
+ }
267
+
268
+ // Example 7: Using apiRaw for full response
269
+ async function example7() {
270
+ const response = await caller.apiRaw('getUserById', { userId: '123' });
271
+
272
+ if (response) {
273
+ console.log('Payload:', response.payload); // User | null
274
+ if (response.logList) {
275
+ console.log('Logs:', response.logList);
276
+ }
277
+ if (response.errorMessage) {
278
+ console.error('Error:', response.errorMessage);
279
+ }
280
+ }
281
+ }
282
+
283
+ return caller;
284
+ }
285
+
286
+ // ============================================================================
287
+ // Without Contract (Backward Compatibility)
288
+ // ============================================================================
289
+
290
+ export function setupWithoutContract() {
291
+ // If you don't pass a contract type, it works like before (untyped)
292
+ const caller = new LambderCaller({
293
+ apiPath: '/api',
294
+ isCorsEnabled: false,
295
+ });
296
+
297
+ // Still works, but no type safety
298
+ async function untypedExample() {
299
+ const user = await caller.api('getUserById', { userId: '123' });
300
+ // user is any
301
+ }
302
+
303
+ const lambder = new Lambder({
304
+ publicPath: './public',
305
+ apiPath: '/api',
306
+ });
307
+
308
+ // Still works, but no type safety
309
+ lambder.addApi('getUserById', async (ctx, resolver) => {
310
+ // ctx.apiPayload is any
311
+ const user = { id: ctx.apiPayload.userId, name: 'User' };
312
+ return resolver.api(user);
313
+ });
314
+ }
315
+
316
+ // ============================================================================
317
+ // Type Safety Examples
318
+ // ============================================================================
319
+
320
+ export function typeSafetyExamples() {
321
+ const caller = new LambderCaller<MyApiContract>({ apiPath: '/api', isCorsEnabled: false });
322
+
323
+ async function examples() {
324
+ // ✓ VALID:
325
+ await caller.api('getUserById', { userId: '123' });
326
+ await caller.api('createUser', { name: 'Alice', email: 'alice@example.com', password: 'pass' });
327
+ await caller.api('listUsers', undefined);
328
+
329
+ // ✗ ERRORS (TypeScript prevents):
330
+ // await caller.api('getUserById'); // Missing required payload
331
+ // await caller.api('getUserById', { id: '123' }); // Wrong property name (should be userId)
332
+ // await caller.api('createUser', { name: 'Bob' }); // Missing email and password
333
+ // await caller.api('nonExistentApi', {}); // API doesn't exist in contract
334
+
335
+ // Type inference works:
336
+ const user = await caller.api('getUserById', { userId: '123' });
337
+ if (user) {
338
+ console.log(user.name); // ✓ TypeScript knows user has name
339
+ // console.log(user.age); // ✗ Error: Property 'age' does not exist
340
+ }
341
+
342
+ const users = await caller.api('listUsers', undefined);
343
+ if (users) {
344
+ users.forEach(u => {
345
+ console.log(u.email); // ✓ TypeScript knows array item structure
346
+ });
347
+ }
348
+ }
349
+ }
350
+
351
+ // ============================================================================
352
+ // Key Benefits
353
+ // ============================================================================
354
+ //
355
+ // 1. ✅ Type Safety - Frontend and backend share the same types
356
+ // 2. ✅ Autocomplete - IDE suggests available APIs as you type
357
+ // 3. ✅ No Wrappers - Use existing api() and addApi() methods
358
+ // 4. ✅ Opt-In - Add types when you want, or don't use them at all
359
+ // 5. ✅ Backward Compatible - Existing code works without changes
360
+ // 6. ✅ Simple - Just pass type to constructor, that's it!
361
+ // 7. ✅ Zero Runtime Overhead - Pure TypeScript types
362
+ //
363
+ // ============================================================================
364
+
365
+ export { Lambder, LambderCaller, type ApiContract };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "1.0.125",
3
+ "version": "1.0.126",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/Lambder.ts CHANGED
@@ -8,6 +8,7 @@ import LambderResponseBuilder, { LambderResolverResponse } from "./LambderRespon
8
8
  import LambderUtils from "./LambderUtils.js";
9
9
  import LambderSessionManager, { type LambderSessionContext } from "./LambderSessionManager.js";
10
10
  import LambderSessionController from "./LambderSessionController.js";
11
+ import type { ApiContract } from "./LambderApiContract.js";
11
12
 
12
13
  type Path = `/${string}`;
13
14
 
@@ -92,7 +93,7 @@ export const createContext = (
92
93
  }
93
94
 
94
95
 
95
- export default class Lambder {
96
+ export default class Lambder<TContract extends ApiContract = any> {
96
97
  public apiPath: string;
97
98
  public apiVersion: null|string;
98
99
  public isCorsEnabled: boolean = false;
@@ -238,6 +239,25 @@ export default class Lambder {
238
239
  });
239
240
  };
240
241
 
242
+ // Overload for untyped API with RegExp or function
243
+ addApi(
244
+ apiName: ConditionFunction|RegExp,
245
+ actionFn: ActionFunction
246
+ ):void;
247
+ // Overload for typed API with string name (must come before untyped string overload)
248
+ addApi<TApiName extends keyof TContract & string>(
249
+ apiName: TApiName,
250
+ actionFn: (
251
+ ctx: LambderRenderContext & { apiPayload: TContract[TApiName]['input'] },
252
+ resolver: LambderResolver
253
+ ) => LambderResolverResponse|Promise<LambderResolverResponse>
254
+ ):void;
255
+ // Overload for untyped API with string (backward compatibility, must be last)
256
+ addApi(
257
+ apiName: string,
258
+ actionFn: ActionFunction
259
+ ):void;
260
+ // Implementation
241
261
  addApi(apiName: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
242
262
  this.actionList.push({
243
263
  conditionFn: (ctx:LambderRenderContext) => (
@@ -251,6 +271,25 @@ export default class Lambder {
251
271
  });
252
272
  };
253
273
 
274
+ // Overload for untyped session API with RegExp or function
275
+ addSessionApi(
276
+ apiName: ConditionFunction|RegExp,
277
+ actionFn: ActionFunction
278
+ ):void;
279
+ // Overload for typed session API with string name (must come before untyped string overload)
280
+ addSessionApi<TApiName extends keyof TContract & string>(
281
+ apiName: TApiName,
282
+ actionFn: (
283
+ ctx: LambderRenderContext & { apiPayload: TContract[TApiName]['input'] },
284
+ resolver: LambderResolver
285
+ ) => LambderResolverResponse|Promise<LambderResolverResponse>
286
+ ):void;
287
+ // Overload for untyped session API with string (backward compatibility, must be last)
288
+ addSessionApi(
289
+ apiName: string,
290
+ actionFn: ActionFunction
291
+ ):void;
292
+ // Implementation
254
293
  addSessionApi(apiName: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
255
294
  this.actionList.push({
256
295
  conditionFn: (ctx:LambderRenderContext) => (
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Type-safe API Contract System
3
+ *
4
+ * Define your API contract as a TypeScript type to get full type safety
5
+ * across frontend and backend with no runtime overhead.
6
+ *
7
+ * Example:
8
+ *
9
+ * export type MyApiContract = {
10
+ * getUserById: { input: { userId: string }, output: User },
11
+ * createUser: { input: CreateUserInput, output: User },
12
+ * listUsers: { input: void, output: User[] }
13
+ * }
14
+ *
15
+ * Frontend:
16
+ * const caller = new LambderCaller<MyApiContract>({ ... });
17
+ * const user = await caller.api('getUserById', { userId: '123' }); // typed!
18
+ *
19
+ * Backend:
20
+ * const lambder = new Lambder<MyApiContract>({ ... });
21
+ * lambder.addApi('getUserById', async (ctx, resolver) => {
22
+ * // ctx.apiPayload is typed as { userId: string }
23
+ * return resolver.api(user); // user is typed as User
24
+ * });
25
+ */
26
+
27
+ /**
28
+ * Base type for API contracts
29
+ */
30
+ export type ApiContract = {
31
+ [apiName: string]: {
32
+ input: any;
33
+ output: any;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Extract input type from contract for a specific API
39
+ */
40
+ export type ApiInput<
41
+ TContract extends ApiContract,
42
+ TApiName extends keyof TContract
43
+ > = TContract[TApiName]['input'];
44
+
45
+ /**
46
+ * Extract output type from contract for a specific API
47
+ */
48
+ export type ApiOutput<
49
+ TContract extends ApiContract,
50
+ TApiName extends keyof TContract
51
+ > = TContract[TApiName]['output'];
@@ -1,5 +1,6 @@
1
1
  import Cookies from 'js-cookie';
2
2
  import { LambderApiResponse } from './LambderResponseBuilder';
3
+ import type { ApiContract } from './LambderApiContract';
3
4
 
4
5
  type VoidFunction = ()=>void|Promise<void>;
5
6
  type FetchTracker = { apiName: string, done: boolean, fetchEndCalled: boolean };
@@ -23,7 +24,7 @@ type FetchEndEventHandler = (params: {
23
24
  type ErrorHandler = (err: Error) => void|Promise<void>;
24
25
  type MessageHandler = (message:any) => void|Promise<void>;
25
26
 
26
- export default class LambderCaller {
27
+ export default class LambderCaller<TContract extends ApiContract = any> {
27
28
  private isCorsEnabled: boolean;
28
29
  private apiPath: string;
29
30
  private apiVersion?: string;
@@ -90,17 +91,24 @@ export default class LambderCaller {
90
91
  this.sessionCsrfCookieKey = sessionCsrfCookieKey;
91
92
  }
92
93
 
93
- async apiRaw<T=any>(apiName: string, payload?: any, options?: {
94
- headers?: Record<string, any>
95
- versionExpiredHandler?: VoidFunction,
96
- sessionExpiredHandler?: VoidFunction,
97
- messageHandler?: MessageHandler,
98
- errorMessageHandler?: MessageHandler,
99
- notAuthorizedHandler?: VoidFunction,
100
- errorHandler?: ErrorHandler,
101
- fetchStartedHandler?: FetchStartEventHandler,
102
- fetchEndedHandler?: FetchEndEventHandler,
103
- }): Promise<LambderApiResponse<T>|null|undefined>{
94
+ async apiRaw<
95
+ TApiName extends keyof TContract & string = string,
96
+ TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any
97
+ >(
98
+ apiName: TApiName,
99
+ payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any,
100
+ options?: {
101
+ headers?: Record<string, any>
102
+ versionExpiredHandler?: VoidFunction,
103
+ sessionExpiredHandler?: VoidFunction,
104
+ messageHandler?: MessageHandler,
105
+ errorMessageHandler?: MessageHandler,
106
+ notAuthorizedHandler?: VoidFunction,
107
+ errorHandler?: ErrorHandler,
108
+ fetchStartedHandler?: FetchStartEventHandler,
109
+ fetchEndedHandler?: FetchEndEventHandler,
110
+ }
111
+ ): Promise<LambderApiResponse<TOutput>|null|undefined>{
104
112
  const headers = options?.headers;
105
113
  const fetchTracker: FetchTracker = { apiName, done: false, fetchEndCalled: false };
106
114
  try {
@@ -125,7 +133,7 @@ export default class LambderCaller {
125
133
  }else{
126
134
  return res.json();
127
135
  }
128
- }) as LambderApiResponse<T>;
136
+ }) as LambderApiResponse<TOutput>;
129
137
  fetchTracker.done = true;
130
138
  if(this.fetchEndedHandler){
131
139
  fetchTracker.fetchEndCalled = true;
@@ -189,8 +197,25 @@ export default class LambderCaller {
189
197
  };
190
198
 
191
199
  // Use the same type for api but adjust the return type
192
- async api<T=any>(...params: Parameters<typeof LambderCaller.prototype.apiRaw>): Promise<T|null|undefined> {
193
- const result = await this.apiRaw<T>(...params);
200
+ async api<
201
+ TApiName extends keyof TContract & string = string,
202
+ TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any
203
+ >(
204
+ apiName: TApiName,
205
+ payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any,
206
+ options?: {
207
+ headers?: Record<string, any>
208
+ versionExpiredHandler?: VoidFunction,
209
+ sessionExpiredHandler?: VoidFunction,
210
+ messageHandler?: MessageHandler,
211
+ errorMessageHandler?: MessageHandler,
212
+ notAuthorizedHandler?: VoidFunction,
213
+ errorHandler?: ErrorHandler,
214
+ fetchStartedHandler?: FetchStartEventHandler,
215
+ fetchEndedHandler?: FetchEndEventHandler,
216
+ }
217
+ ): Promise<TOutput|null|undefined> {
218
+ const result = await this.apiRaw<TApiName, TOutput>(apiName, payload, options);
194
219
  return result?.payload;
195
220
  }
196
221
 
package/src/index.ts CHANGED
@@ -5,3 +5,10 @@ export { default as LambderCaller } from "./LambderCaller.js";
5
5
  export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
6
6
  export { default as LambderResolver } from "./LambderResolver.js";
7
7
  export { default as LambderSessionManager } from "./LambderSessionManager.js";
8
+
9
+ // Type-safe API contract utilities
10
+ export {
11
+ type ApiContract,
12
+ type ApiInput,
13
+ type ApiOutput,
14
+ } from "./LambderApiContract.js";
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Type Safety Tests
3
+ *
4
+ * This file tests that the type system works correctly.
5
+ * If this file compiles without errors, the types are working!
6
+ */
7
+
8
+ import Lambder from '../src/Lambder.js';
9
+ import LambderCaller from '../src/LambderCaller.js';
10
+ import type { ApiContract } from '../src/index.js';
11
+
12
+ // Test contract
13
+ type TestApiContract = {
14
+ getUser: { input: { userId: string }, output: { id: string, name: string } },
15
+ createUser: { input: { name: string, email: string }, output: { id: string, name: string, email: string } },
16
+ listUsers: { input: void, output: Array<{ id: string, name: string }> },
17
+ deleteUser: { input: { userId: string }, output: boolean }
18
+ }
19
+
20
+ // ============================================================================
21
+ // Test 1: LambderCaller Type Safety
22
+ // ============================================================================
23
+
24
+ function testCallerTypes() {
25
+ const caller = new LambderCaller<TestApiContract>({
26
+ apiPath: '/api',
27
+ isCorsEnabled: false
28
+ });
29
+
30
+ async function test() {
31
+ // ✅ Should work: Correct types
32
+ const user1 = await caller.api('getUser', { userId: '123' });
33
+ const user2 = await caller.api('createUser', { name: 'Alice', email: 'alice@example.com' });
34
+ const users = await caller.api('listUsers', undefined);
35
+ const deleted = await caller.api('deleteUser', { userId: '123' });
36
+
37
+ // Type assertions to verify return types
38
+ if (user1) {
39
+ const name: string = user1.name; // Should work
40
+ const id: string = user1.id; // Should work
41
+ }
42
+
43
+ if (user2) {
44
+ const email: string = user2.email; // Should work
45
+ }
46
+
47
+ if (users) {
48
+ const firstUser = users[0];
49
+ if (firstUser) {
50
+ const name: string = firstUser.name; // Should work
51
+ }
52
+ }
53
+
54
+ if (deleted !== null && deleted !== undefined) {
55
+ const result: boolean = deleted; // Should work
56
+ }
57
+
58
+ // ❌ These should cause TypeScript errors (commented out to allow compilation):
59
+
60
+ // Wrong property name
61
+ // await caller.api('getUser', { id: '123' }); // Error: should be userId
62
+
63
+ // Missing required property
64
+ // await caller.api('createUser', { name: 'Bob' }); // Error: missing email
65
+
66
+ // Wrong type
67
+ // await caller.api('getUser', { userId: 123 }); // Error: userId should be string
68
+
69
+ // Non-existent API
70
+ // await caller.api('nonExistent', {}); // Error: API doesn't exist
71
+
72
+ // Wrong payload for void input
73
+ // await caller.api('listUsers', { something: true }); // Error: should be undefined
74
+ }
75
+ }
76
+
77
+ // ============================================================================
78
+ // Test 2: Lambder Type Safety
79
+ // ============================================================================
80
+
81
+ function testLambderTypes() {
82
+ const lambder = new Lambder<TestApiContract>({
83
+ publicPath: './public',
84
+ apiPath: '/api'
85
+ });
86
+
87
+ // ✅ Should work: Typed API
88
+ lambder.addApi('getUser', async (ctx, resolver) => {
89
+ // ctx.apiPayload should be typed as { userId: string }
90
+ const userId: string = ctx.apiPayload.userId; // Should work
91
+
92
+ // Return correct type
93
+ return resolver.api({ id: userId, name: 'Test' });
94
+ });
95
+
96
+ // ✅ Should work: Typed session API
97
+ lambder.addSessionApi('createUser', async (ctx, resolver) => {
98
+ // ctx.apiPayload should be typed as { name: string, email: string }
99
+ const name: string = ctx.apiPayload.name;
100
+ const email: string = ctx.apiPayload.email;
101
+
102
+ return resolver.api({ id: '1', name, email });
103
+ });
104
+
105
+ // ✅ Should work: Void input API
106
+ lambder.addApi('listUsers', async (ctx, resolver) => {
107
+ // ctx.apiPayload should be void/undefined
108
+ return resolver.api([
109
+ { id: '1', name: 'User 1' },
110
+ { id: '2', name: 'User 2' }
111
+ ]);
112
+ });
113
+
114
+ // ✅ Should work: RegExp (untyped)
115
+ lambder.addApi(/^admin\./, async (ctx, resolver) => {
116
+ // ctx.apiPayload is any (untyped)
117
+ return resolver.api({});
118
+ });
119
+
120
+ // ❌ These should cause TypeScript errors (commented out):
121
+
122
+ // Accessing wrong property
123
+ // lambder.addApi('getUser', async (ctx, resolver) => {
124
+ // const id = ctx.apiPayload.id; // Error: should be userId
125
+ // return resolver.api({ id, name: 'Test' });
126
+ // });
127
+
128
+ // Returning wrong type
129
+ // lambder.addApi('getUser', async (ctx, resolver) => {
130
+ // return resolver.api({ id: '1' }); // Error: missing name property
131
+ // });
132
+ }
133
+
134
+ // ============================================================================
135
+ // Test 3: Backward Compatibility (No Contract)
136
+ // ============================================================================
137
+
138
+ function testBackwardCompatibility() {
139
+ // Without contract type - should work as before (untyped)
140
+ const caller = new LambderCaller({
141
+ apiPath: '/api',
142
+ isCorsEnabled: false
143
+ });
144
+
145
+ const lambder = new Lambder({
146
+ publicPath: './public',
147
+ apiPath: '/api'
148
+ });
149
+
150
+ async function test() {
151
+ // Should work - untyped
152
+ const result = await caller.api('anyApi', { anything: true });
153
+
154
+ lambder.addApi('anyApi', async (ctx, resolver) => {
155
+ // ctx.apiPayload is any
156
+ return resolver.api({ anything: ctx.apiPayload });
157
+ });
158
+ }
159
+ }
160
+
161
+ // ============================================================================
162
+ // Test 4: apiRaw Method
163
+ // ============================================================================
164
+
165
+ function testApiRaw() {
166
+ const caller = new LambderCaller<TestApiContract>({
167
+ apiPath: '/api',
168
+ isCorsEnabled: false
169
+ });
170
+
171
+ async function test() {
172
+ // apiRaw should also be typed
173
+ const response = await caller.apiRaw('getUser', { userId: '123' });
174
+
175
+ if (response) {
176
+ // response.payload should be typed
177
+ const user = response.payload;
178
+ if (user) {
179
+ const name: string = user.name; // Should work
180
+ const id: string = user.id; // Should work
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ // ============================================================================
187
+ // Test 5: Complex Types
188
+ // ============================================================================
189
+
190
+ type ComplexApiContract = {
191
+ update: {
192
+ input: { id: string } & Partial<{ name: string, email: string }>,
193
+ output: { id: string, name: string, email: string }
194
+ },
195
+ search: {
196
+ input: { query: string, filters?: { role?: string, active?: boolean } },
197
+ output: Array<{ id: string, name: string }>
198
+ }
199
+ }
200
+
201
+ function testComplexTypes() {
202
+ const caller = new LambderCaller<ComplexApiContract>({
203
+ apiPath: '/api',
204
+ isCorsEnabled: false
205
+ });
206
+
207
+ async function test() {
208
+ // Should work with partial update
209
+ await caller.api('update', { id: '1', name: 'New Name' });
210
+ await caller.api('update', { id: '1', email: 'new@email.com' });
211
+ await caller.api('update', { id: '1' }); // Only id
212
+
213
+ // Should work with optional nested object
214
+ await caller.api('search', { query: 'test' });
215
+ await caller.api('search', { query: 'test', filters: { role: 'admin' } });
216
+ await caller.api('search', { query: 'test', filters: { role: 'admin', active: true } });
217
+ }
218
+ }
219
+
220
+ // ============================================================================
221
+ // If this file compiles without errors, the type system is working! ✅
222
+ // ============================================================================
223
+
224
+ export { };