lambder 1.0.148 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/Readme.md +355 -410
  2. package/dist/Lambder.d.ts +47 -21
  3. package/dist/Lambder.js +79 -24
  4. package/dist/LambderApiContract.d.ts +10 -42
  5. package/dist/LambderApiContract.js +2 -22
  6. package/dist/LambderCaller.js +2 -2
  7. package/dist/LambderMSW.js +0 -2
  8. package/dist/LambderResolver.d.ts +16 -17
  9. package/dist/LambderResponseBuilder.d.ts +2 -3
  10. package/dist/LambderResponseBuilder.js +1 -3
  11. package/dist/LambderUtils.js +1 -3
  12. package/dist/index.d.ts +1 -1
  13. package/docs/LAMBDER_MSW.md +6 -6
  14. package/docs/TYPE_SAFE_QUICK_START.md +54 -177
  15. package/examples/msw-testing-example.ts +36 -33
  16. package/examples/secure-session-example.ts +50 -34
  17. package/examples/zod-chained-api-example.ts +63 -0
  18. package/package.json +3 -2
  19. package/src/Lambder.ts +124 -83
  20. package/src/LambderApiContract.ts +7 -50
  21. package/src/LambderCaller.ts +2 -2
  22. package/src/LambderMSW.ts +0 -4
  23. package/src/LambderResolver.ts +21 -24
  24. package/src/LambderResponseBuilder.ts +4 -7
  25. package/src/LambderUtils.ts +1 -3
  26. package/src/index.ts +0 -3
  27. package/tests/UNTESTED_FEATURES.md +263 -0
  28. package/tests/error-handling.test.ts +585 -0
  29. package/tests/hooks.test.ts +561 -0
  30. package/tests/output-type-runtime.test.ts +80 -64
  31. package/tests/routes.test.ts +542 -0
  32. package/tests/session.test.ts +38 -24
  33. package/tests/type-safety.test.ts +147 -97
  34. package/tests/use-plugin.test.ts +437 -0
  35. package/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +0 -90
  36. package/examples/output-type-enforcement-example.ts +0 -218
  37. package/examples/simplified-typed-api-example.ts +0 -365
  38. package/examples/test-output-type-enforcement.ts +0 -101
  39. package/test-type-enforcement.ts +0 -111
package/Readme.md CHANGED
@@ -1,23 +1,25 @@
1
- # Lambder - Serverless NodeJS Web Framework
1
+ # Lambder - Serverless NodeJS Web Framework (v2.0)
2
2
 
3
3
  Lambder is a highly opinionated dynamic serverless framework designed to facilitate the management and implementation of routes and APIs within AWS Lambda functions, specifically tailored for TypeScript projects. It provides a streamlined approach to handling HTTP requests, managing sessions, and defining API routes, making serverless application development more intuitive and structured.
4
4
 
5
+ **New in v2.0:** Full type safety with Zod schemas and runtime validation!
6
+
5
7
  ## Features
6
8
 
9
+ - **Type-Safe APIs with Zod**: Define inputs and outputs with Zod schemas. Get automatic runtime validation and compile-time type inference.
10
+ - **Method Chaining**: Build your API contract incrementally with a fluent interface.
7
11
  - **Simple API & Route Declaration**: Define your APIs and routes using concise and expressive syntax.
8
12
  - **Session Management**: Built-in session management to secure and personalize user experiences.
9
- - **Flexible Hooks System**: Employ hooks to execute code at different stages of the request lifecycle, enabling fine-grained control over the application flow.
13
+ - **Flexible Hooks System**: Employ hooks to execute code at different stages of the request lifecycle.
10
14
  - **Error Handling**: Comprehensive error handling capabilities, including global error handlers and route-specific fallbacks.
11
- - **Seamless Integration**: Designed to work effortlessly with AWS Lambda and API Gateway, providing a straightforward path to deploy serverless applications.
15
+ - **Seamless Integration**: Designed to work effortlessly with AWS Lambda and API Gateway.
12
16
 
13
17
  ## Installation
14
18
 
15
- To include Lambder in your TypeScript project, you can install it using npm or yarn. First, ensure that you have TypeScript set up in your project.
16
-
17
19
  ```bash
18
- npm install lambder
20
+ npm install lambder zod
19
21
  # or
20
- yarn add lambder
22
+ yarn add lambder zod
21
23
  ```
22
24
 
23
25
  ## Backend Usage
@@ -25,296 +27,300 @@ yarn add lambder
25
27
  ### Basic Setup
26
28
 
27
29
  ```typescript
28
- import Lambder from 'lambder';
30
+ import Lambder, { InferLambderContract } from 'lambder';
31
+ import { z } from 'zod';
29
32
  import * as path from 'path';
30
33
 
31
34
  const lambder = new Lambder({
32
- apiPath: "/secure",
35
+ apiPath: "/api",
33
36
  publicPath: path.resolve(`./public`),
34
- // ejsPath: path.resolve(`./ejs-templates`),
35
- });
36
-
37
-
38
- // Enable session
39
- lambder.enableDdbSession({
40
- tableName: "website-session", // DynamoDB Table Name
41
- tableRegion: "us-east-1", // DynamoDB Table Region
42
- sessionSalt: "8p6Vt+4b1w3N8d/dcJ47QF3DRkp9koFg0G" // Change salt
43
- });
44
-
45
- // Enable Cors
46
- lambder.enableCors(true);
47
-
48
- // Define a simple api
49
- lambder.addApi("getCompanyPage", async ({ apiPayload }, res) => {
50
- const companyName = apiPayload.companyName;
51
- const data = await fetchDataSomehow(companyName);
52
- return res.api(data);
53
- });
54
-
55
- // Start a session from an API
56
- lambder.addApi("loginUser", async (ctx, res) => {
57
- const user = await fetchUserData();
58
- await lambder.getSessionController(ctx).createSession(user.id);
59
- return res.api({ success: true });
60
- });
61
-
62
- // Define a simple route
63
- lambder.addRoute("/hello-world", (ctx, res) => {
64
- return res.html("Hello World");
65
- });
66
-
67
- // Route with parameters
68
- lambder.addRoute("/user/:userId", async (ctx, res) => {
69
- const user = await getUser(ctx.pathParams.userId);
70
- if(!user) return res.status404("Not found");
71
-
72
- return res.html(`Hello ${user.name}`);
73
- });
74
-
75
- // Define a regex route
76
- lambder.addRoute(/\/hello-regex/, (ctx, res) => {
77
- return res.html("Hello Regex");
78
- });
79
-
80
- // Function routes allows routing on any context variable.
81
- lambder.addRoute((ctx)=>ctx.path === '/hello-fn-route', (ctx, res) => {
82
- return res.html("Hello from a function route");
83
- });
84
-
85
- // Define a simple route that serves an EJS template file
86
- lambder.addRoute("/product/:productId", (ctx, res) => {
87
- const product = await getProduct(ctx.pathParams.productId);
88
- // Serve the file from ejsPath defined above.
89
- return await res.ejsFile("productPage.html.ejs", { product });
90
- });
91
-
92
- // Serve sitemap using an ejs template.
93
- lambder.addRoute("/sitemap", (ctx, res) => {
94
- const templateString = `
95
- <?xml version="1.0" encoding="UTF-8"?>
96
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
97
- <%~
98
- page.urlList.map(url => `<url><loc>${url}</loc></url>`).join("")
99
- %>
100
- </urlset>
101
- `.trim();
102
- const urlList = [];
103
- return await res.ejsTemplate(templateString, { urlList }, { "Content-Type": ["application/xml; charset=utf-8"]});
104
- });
105
-
106
-
107
- // Match all other paths and serve static files from publicPath, if not found, serve index.html
108
- lambder.addRoute("/(.*)", (ctx, res)=>{
109
- return res.file(ctx.path, {}, "index.html");
110
- });
111
-
112
-
113
- // Set a fallback handler for unmatched routes
114
- lambder.setRouteFallbackHandler((ctx, res) => {
115
- return res.status404("Not Found");
37
+ // ejsPath: path.resolve(`./ejs-templates`), // Optional
116
38
  });
117
39
 
118
- // Global error handler
119
- lambder.setGlobalErrorHandler((err, ctx, res) => {
120
- console.error("Error:", err);
121
- return res.status500("Internal Server Error");
122
- });
123
-
124
- export const handler = (event, context) => {
125
- return lambder.render(event, context);
126
- };
40
+ // Enable session and CORS - all chainable!
41
+ lambder
42
+ .enableDdbSession({
43
+ tableName: "website-session",
44
+ tableRegion: "us-east-1",
45
+ sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING"
46
+ })
47
+ .enableCors(true);
48
+
49
+ // Define type-safe APIs with Zod schemas
50
+ const app = lambder
51
+ .addApi("getCompanyPage", {
52
+ input: z.object({ companyName: z.string() }),
53
+ output: z.object({ id: z.string(), name: z.string(), description: z.string() })
54
+ }, async ({ apiPayload }, res) => {
55
+ // apiPayload is automatically typed and validated!
56
+ const data = await fetchDataSomehow(apiPayload.companyName);
57
+ return res.api(data); // Return value is type-checked
58
+ })
59
+ .addApi("loginUser", {
60
+ input: z.object({ email: z.string().email(), password: z.string() }),
61
+ output: z.object({ success: z.boolean(), token: z.string().optional() })
62
+ }, async (ctx, res) => {
63
+ const user = await authenticateUser(ctx.apiPayload.email, ctx.apiPayload.password);
64
+ if (!user) {
65
+ return res.api({ success: false });
66
+ }
67
+
68
+ await lambder.getSessionController(ctx).createSession(user.id);
69
+ return res.api({ success: true, token: "session-token" });
70
+ });
71
+
72
+ // Export the inferred contract for the frontend
73
+ export type AppContract = typeof lambder.ApiContractType;
74
+
75
+ // Export the handler
76
+ export const handler = lambder.getHandler();
127
77
  ```
128
78
 
79
+ ### Adding Routes
129
80
 
81
+ Routes are fully chainable for a fluent interface.
130
82
 
131
- ### Adding APIs
132
-
133
- For more details on route matching, please check [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) package.
134
83
  ```typescript
135
- // Add routes
136
- lambder.addRoute(pathAsString, async (ctx, res) => {});
137
- lambder.addRoute(pathAsRegex, async (ctx, res) => {});
138
- lambder.addRoute(matchFunction, async (ctx, res) => {});
139
-
140
- // Add apis
141
- lambder.addApi(apiNameAsString, async (ctx, res) => {});
142
- lambder.addApi(apiNameAsRegex, async (ctx, res) => {});
143
- lambder.addApi(matchFunction, async (ctx, res) => {});
144
-
145
- // Add only session accessible routes
146
- lambder.addSessionRoute(pathAsString, async (ctx, res) => {});
147
- lambder.addSessionRoute(pathAsRegex, async (ctx, res) => {});
148
- lambder.addSessionRoute(matchFunction, async (ctx, res) => {});
149
-
150
- // Add only session accessible apis
151
- lambder.addSessionApi(apiNameAsString, async (ctx, res) => {});
152
- lambder.addSessionApi(apiNameAsRegex, async (ctx, res) => {});
153
- lambder.addSessionApi(matchFunction, async (ctx, res) => {});
154
-
84
+ lambder
85
+ // Define a simple route
86
+ .addRoute("/hello-world", (ctx, res) => {
87
+ return res.html("Hello World");
88
+ })
89
+ // Route with parameters
90
+ .addRoute("/user/:userId", async (ctx, res) => {
91
+ const user = await getUser(ctx.pathParams.userId);
92
+ if(!user) return res.status404("Not found");
93
+ return res.html(`Hello ${user.name}`);
94
+ })
95
+ // Define a regex route
96
+ .addRoute(/\/hello-regex/, (ctx, res) => {
97
+ return res.html("Hello Regex");
98
+ })
99
+ // Function routes allows routing on any context variable
100
+ .addRoute((ctx)=>ctx.path === '/hello-fn-route', (ctx, res) => {
101
+ return res.html("Hello from a function route");
102
+ })
103
+ // Define a simple route that serves an EJS template file
104
+ .addRoute("/product/:productId", async (ctx, res) => {
105
+ const product = await getProduct(ctx.pathParams.productId);
106
+ // Serve the file from ejsPath defined above.
107
+ return await res.ejsFile("productPage.html.ejs", { product });
108
+ })
109
+ // Serve sitemap using an ejs template
110
+ .addRoute("/sitemap", async (ctx, res) => {
111
+ const templateString = `
112
+ <?xml version="1.0" encoding="UTF-8"?>
113
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
114
+ <%~
115
+ page.urlList.map(url => \`<url><loc>\${url}</loc></url>\`).join("")
116
+ %>
117
+ </urlset>
118
+ `.trim();
119
+ const urlList = await getUrlList();
120
+ return await res.ejsTemplate(templateString, { urlList }, { "Content-Type": ["application/xml; charset=utf-8"]});
121
+ })
122
+ // Match all other paths and serve static files from publicPath
123
+ .addRoute("/(.*)", (ctx, res)=>{
124
+ return res.file(ctx.path, {}, "index.html");
125
+ })
126
+ // Set a fallback handler for unmatched routes
127
+ .setRouteFallbackHandler((ctx, res) => {
128
+ return res.status404("Not Found");
129
+ })
130
+ // Global error handler
131
+ .setGlobalErrorHandler((err, ctx, res) => {
132
+ console.error("Error:", err);
133
+ return res.raw({ statusCode: 500, body: "Internal Server Error" });
134
+ });
155
135
  ```
156
136
 
137
+ ### Adding APIs
157
138
 
158
- ```typescript
159
- // Define a simple api
160
- lambder.addApi("getCompanyPage", async (ctx, res) => {
161
- const {
162
- host, path, get, post, cookie, headers,
163
- apiName, apiPayload
164
- } = ctx;
165
- const companyName = apiPayload.companyName;
166
- const data = await fetchDataSomehow(companyName);
167
- return res.api(data);
168
- });
169
- ```
139
+ All APIs in v2.0 must use Zod schemas for type safety and runtime validation. Use method chaining for a clean API definition.
170
140
 
171
141
  ```typescript
172
- // Define an only session accessible api
173
- lambder.addSessionApi("getCompanyPage", async (ctx, res) => {
174
- const {
175
- host, path, get, post, cookie, headers,
176
- session, apiName, apiPayload
177
- } = ctx;
178
-
179
- const companyName = session.data.companyName;
180
- const data = await fetchDataSomehow(companyName);
181
- return res.api(data);
182
- });
142
+ import { z } from 'zod';
143
+
144
+ lambder
145
+ // Add a typed API
146
+ .addApi("getUserById", {
147
+ input: z.object({ userId: z.string() }),
148
+ output: z.object({ id: z.string(), name: z.string(), email: z.string() })
149
+ }, async (ctx, res) => {
150
+ // ctx.apiPayload is typed as { userId: string }
151
+ const user = await db.getUser(ctx.apiPayload.userId);
152
+ return res.api(user); // Type-checked against output schema
153
+ })
154
+ // Session-protected API
155
+ .addSessionApi("getProfile", {
156
+ input: z.void(),
157
+ output: z.object({ userId: z.string(), username: z.string() })
158
+ }, async (ctx, res) => {
159
+ // Session is automatically fetched and validated
160
+ // ctx.session.data contains your session data
161
+ return res.api({
162
+ userId: ctx.session.data.userId,
163
+ username: ctx.session.data.username
164
+ });
165
+ });
183
166
  ```
184
167
 
168
+ ### Modular APIs with .use()
185
169
 
170
+ For larger applications, split your APIs into separate modules:
186
171
 
187
- ### Adding Routes
188
172
  ```typescript
189
- // Route with parameters
190
- lambder.addRoute("/user/:userId", async (ctx, res) => {
191
- const user = await getUser(ctx.pathParams.userId);
192
- if(!user) return res.status404("Not found");
193
-
194
- return res.html(`Hello ${user.name}`);
195
- });
196
- ```
173
+ // user-api.ts
174
+ import { z } from "zod";
175
+ import Lambder from "lambder";
176
+
177
+ export const userApi = <T>(l: Lambder<T>) => {
178
+ return l
179
+ .addApi("getUser", {
180
+ input: z.object({ id: z.string() }),
181
+ output: z.object({ id: z.string(), name: z.string() })
182
+ }, async (ctx, res) => {
183
+ return res.api({ id: ctx.apiPayload.id, name: "User" });
184
+ })
185
+ .addApi("createUser", {
186
+ input: z.object({ name: z.string(), email: z.string() }),
187
+ output: z.object({ id: z.string() })
188
+ }, async (ctx, res) => {
189
+ return res.api({ id: "123" });
190
+ });
191
+ };
197
192
 
193
+ // index.ts
194
+ import { userApi } from "./user-api";
198
195
 
199
- ### Adding Hooks
196
+ const lambder = new Lambder()
197
+ .use(userApi);
200
198
 
201
- ```typescript
202
- // Before render hook example
203
- lambder.addHook("beforeRender", async (ctx, res) => {
204
- // Perform actions before rendering
205
- return ctx; // Return modified context or throw an Error
206
- });
199
+ export type AppContract = typeof lambder.ApiContractType;
207
200
  ```
208
201
 
209
202
 
210
- ### Add Module
203
+ ### Hooks
204
+
205
+ Lambder provides hooks to execute code at different stages of the request lifecycle.
211
206
 
212
207
  ```typescript
213
- lambder.addModule(async (lambder: Lambder): Promise<void> => {
214
- // lambder.addHook(...)
215
- // lambder.addRoute(...)
216
- });
208
+ lambder
209
+ // Before render hook
210
+ .addHook("beforeRender", async (ctx, res) => {
211
+ // Perform actions before rendering
212
+ console.log("Request received:", ctx.path);
213
+ return ctx; // Return modified context or throw an Error
214
+ })
215
+ // After render hook
216
+ .addHook("afterRender", async (ctx, res, response) => {
217
+ // Modify response before sending
218
+ console.log("Response status:", response.statusCode);
219
+ return response;
220
+ });
217
221
  ```
218
222
 
219
223
  ### Session Management
220
224
 
221
- You can enable session tracking by:
225
+ Enable DynamoDB-based session management - fully chainable:
222
226
 
223
227
  ```typescript
224
- // Enable sessions using a dynamodb session table
225
- lambder.enableDdbSession({
226
- tableName: "website-session", // DynamoDB Table Name
227
- tableRegion: "us-east-1", // DynamoDB Table Region
228
- sessionSalt: "8p6Vt+4b1w3N8d/dcJ47QF3DRkp9koFg0G" // Change salt
229
- });
228
+ // Enable sessions using a DynamoDB session table - chainable!
229
+ const lambder = new Lambder({ apiPath: '/api', publicPath: './public' })
230
+ .enableDdbSession({
231
+ tableName: "website-session",
232
+ tableRegion: "us-east-1",
233
+ sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING",
234
+ enableSlidingExpiration: true // Optional: extend session on each access
235
+ })
236
+ .enableCors(true)
237
+ .addApi(...);
230
238
  ```
231
- #### DynamoDB Session Table Structure:
232
239
 
233
- Session system is enabled by storing data in a DynamoDB session table.
240
+ #### DynamoDB Session Table Structure
234
241
 
235
242
  - Primary Key: "pk"
236
243
  - Sort Key: "sk"
237
- - TTL Key: "expiresAt" (optional)
244
+ - TTL Key: "expiresAt" (optional, recommended)
245
+
246
+ See [docs/DYNAMODB_SETUP.md](docs/DYNAMODB_SETUP.md) for detailed setup instructions.
238
247
 
239
248
  #### Session Controller
240
249
 
241
- After you enable the session, you can access to the session controller:
250
+ After enabling sessions, you can access the session controller:
242
251
 
243
252
  ```typescript
244
- // Create session controller:
245
- const sessionController = lambder.getSessionController(ctx);
253
+ const sessionController = lambder.getSessionController(ctx);
246
254
 
247
- // Type for sessionController
248
- sessionController: {
249
- async createSession(sessionKey, data, ttlInSeconds): session
250
- // Starts a new session and persists the session data to DDB.
255
+ // Available methods:
256
+ await sessionController.createSession(sessionKey, data, ttlInSeconds);
257
+ // Starts a new session and persists the session data to DDB.
251
258
 
252
- async fetchSession(): session
253
- // Fetch and validate if there is an existing session
254
- // This is automatically done for addSessionRoute and addSessionApi
255
- // Throws if session not found
259
+ await sessionController.fetchSession();
260
+ // Fetch and validate if there is an existing session
261
+ // This is automatically done for addSessionRoute and addSessionApi
262
+ // Throws if session not found
256
263
 
257
- async fetchSessionIfExists(): session|null
258
- // Runs fetchSession and returns the session if found. Otherwise return null
264
+ await sessionController.fetchSessionIfExists();
265
+ // Returns session if found, otherwise null
259
266
 
260
- async updateSessionData(updatedData): updatedSession
261
- // Updates the active sessions data and persist it to ddb.
267
+ await sessionController.updateSessionData(updatedData);
268
+ // Updates the active session's data and persists it to DDB
262
269
 
263
- async endSession():
264
- // End session and delete from DDB.
265
-
266
- async endSessionAll():
267
- // Ends and deletes all registered sessions for this sessionKey across all devices
268
- }
269
- */
270
- ```
270
+ await sessionController.endSession();
271
+ // End session and delete from DDB
271
272
 
273
+ await sessionController.endSessionAll();
274
+ // Ends and deletes all sessions for this sessionKey across all devices
272
275
 
276
+ await sessionController.regenerateSession();
277
+ // Regenerates session token (use after password change, etc.)
278
+ ```
273
279
 
274
280
  #### Session Examples
275
- ```typescript
276
- lambder.addApi("getCompanyPage", async (ctx, res) => {
277
-
278
- // createSession: Start a new session
279
- const userId = "37234";
280
- await lambder.getSessionController(ctx)
281
- .createSession(userId, { "business": "Session data goes here" });
282
- console.log(ctx.session?.sessionKey); // "37234"
283
- console.log(ctx.session?.data?.business); // "Session data goes here"
284
-
285
- // fetchSession: Fetch and validate if there is an existing session
286
- // This is automatically done for addSessionRoute and addSessionApi
287
- await lambder.getSessionController(ctx).fetchSession();
288
- console.log(ctx.session?.sessionKey); // "37234"
289
-
290
- // updateSessionData: Updates the active sessions data and persist it to ddb.
291
- await lambder.getSessionController(ctx)
292
- .updateSessionData({ "business2": "Session data updated" });
293
- console.log(ctx.session?.sessionKey); // "37234"
294
- console.log(ctx.session?.data?.business); // undefined
295
- console.log(ctx.session?.data?.business2); // "Session data updated"
296
-
297
- // endSession: Ends the session and removes it from ddb
298
- await lambder.getSessionController(ctx).endSession(); // End session
299
- console.log(ctx.session?.sessionKey); // undefined
300
- console.log(ctx.session?.data?.business); // undefined
301
-
302
- // endSessionAll: Ends all registered sessions for this user in all devices.
303
- await lambder.getSessionController(ctx).endSessionAll();
304
- console.log(ctx.session?.sessionKey); // undefined
305
- console.log(ctx.session?.data?.business); // undefined
306
281
 
307
- });
282
+ ```typescript
283
+ lambder
284
+ .addApi("createSession", {
285
+ input: z.object({ userId: z.string() }),
286
+ output: z.object({ success: z.boolean() })
287
+ }, async (ctx, res) => {
288
+ // Create a new session
289
+ const userId = ctx.apiPayload.userId;
290
+ await lambder.getSessionController(ctx)
291
+ .createSession(userId, { business: "Session data goes here" });
292
+
293
+ console.log(ctx.session?.sessionKey); // userId
294
+ console.log(ctx.session?.data?.business); // "Session data goes here"
295
+
296
+ return res.api({ success: true });
297
+ })
298
+ .addSessionApi("updateSession", {
299
+ input: z.object({ newData: z.string() }),
300
+ output: z.object({ success: z.boolean() })
301
+ }, async (ctx, res) => {
302
+ // Session is automatically fetched
303
+ console.log(ctx.session.sessionKey); // userId
304
+
305
+ // Update session data
306
+ await lambder.getSessionController(ctx)
307
+ .updateSessionData({ business2: ctx.apiPayload.newData });
308
+
309
+ console.log(ctx.session.data.business); // undefined
310
+ console.log(ctx.session.data.business2); // newData value
311
+
312
+ return res.api({ success: true });
313
+ });
308
314
  ```
309
315
 
310
- ### EJS Templates:
316
+ ### EJS Templates
311
317
 
312
318
  EJS templates have the variables `page` and `partial` available:
313
319
 
314
- A template is the main file that you call with `await res.ejsFile('template-file')`. Templates will have `page` variable available.
315
- A partial is included from a template, like `<%- await include('partial/header.html.ejs', partialData) -%>` will have `page` and `partial` variables available.
320
+ - **Template**: The main file called with `await res.ejsFile('template-file')`. Has `page` variable.
321
+ - **Partial**: Included from a template with `<%- await include('partial/header.html.ejs', partialData) -%>`. Has both `page` and `partial` variables.
316
322
 
317
- An example template:
323
+ Example template:
318
324
  ```html
319
325
  <div>
320
326
  <%- await include('partial/header.html.ejs', partialData) -%>
@@ -322,7 +328,8 @@ An example template:
322
328
  <%- await include('partial/footer.html.ejs', partialData) -%>
323
329
  </div>
324
330
  ```
325
- An example partial:
331
+
332
+ Example partial:
326
333
  ```html
327
334
  <div>
328
335
  <div>Page Variable: <pre><%~ JSON.stringify(page, null, 2) %></pre></div>
@@ -330,236 +337,170 @@ An example partial:
330
337
  </div>
331
338
  ```
332
339
 
333
- ### Project Structure:
340
+ ### Render Context (ctx) Variables
334
341
 
335
- Add your imports to index.ts:
336
342
  ```typescript
337
- lambder.importModule(import("api.user.js"));
343
+ lambder
344
+ .addApi("exampleApi", {
345
+ input: z.object({ value: z.string() }),
346
+ output: z.object({ result: z.string() })
347
+ }, async (ctx, res) => {
348
+ const {
349
+ host, // Request host: "www.example.com"
350
+ path, // Request path: "/api"
351
+ get, // GET query parameters: { userId: "342" }
352
+ post, // POST body (parsed): { userId: "342" }
353
+ cookie, // Cookies: { "rememberMe": "true" }
354
+ headers, // Request headers
355
+ apiName, // API name: "exampleApi"
356
+ apiPayload, // Validated input (same as post.payload)
357
+ session, // Session (null for addApi, available for addSessionApi)
358
+ } = ctx;
359
+
360
+ return res.api({ result: ctx.apiPayload.value });
361
+ });
338
362
  ```
339
363
 
340
- User related functions can now be added to api.user.ts:
364
+ ### Resolver Methods
365
+
366
+ Available response methods:
367
+
341
368
  ```typescript
342
- import type Lambder from "lambder";
369
+ return res.raw(param);
370
+ // Sends a custom HTTP response. Useful for non-standard responses.
343
371
 
344
- export default (lambder: Lambder): void => {
345
- // lambder.addApi(...)
346
- // lambder.addApi(...)
347
- };
348
- ```
372
+ return res.json(data, headers);
373
+ // Sends a JSON response with optional headers.
349
374
 
350
- Note: importModule is just a wrapper for addModule. `lambder.importModule(import("api.user.js"));` is same as `lambder.addModule((await import("api.user.js")).default);`
375
+ return res.xml(data);
376
+ // Sends an XML response (base64 encoded).
351
377
 
352
- ### Render Context (ctx) Variables
353
- ```typescript
354
- lambder.addApi("getCompanyName", async (ctx, res) => {
355
- const {
356
- host, // Request host. Exp: "www.example.com"
357
- path, // Request path. Exp: "/index.html" or "/user/342"
358
- get, // Get request query in JSON format. Exp: { userId: 342 }
359
- post, // Post request body after JSON parsed. Exp: { userId: 342 }
360
- cookie, // Cookies in an object. Exp: { "rememberMe": "true" }
361
- headers, // Request Headers in an object. Exp: { "User-Agent": "....", ... }
362
- apiName, // In this function it would return "getCompanyName"
363
- apiPayload, // Same as post.payload
364
- session, // Stores session. Only available in addSessionRoute and addSessionApi, otherwise null.
365
- } = ctx;
366
- return res.json({});
367
- });
368
- ```
378
+ return res.html(data, headers);
379
+ // Sends an HTML response (base64 encoded).
369
380
 
370
- ### Resolver Methods
381
+ return res.status301(url, headers);
382
+ // Redirects to the specified URL with a 301 status code.
371
383
 
372
- ```typescript
373
- lambder.addApi("getCompanyName", async (ctx, res) => {
374
- return res.raw(param);
375
- // Sends a custom HTTP response defined by the param object.
376
- // Useful for sending non-standard responses.
377
- return res.json(data, headers);
378
- // Sends a JSON response with the specified data and optional headers.
379
- // It sets the Content-Type header to application/json.
380
-
381
- return res.xml(data);
382
- // Sends an XML response with the given data.
383
- // Automatically encodes the response in base64 and sets
384
- // the Content-Type header to application/xml.
385
-
386
- return res.html(data, headers);
387
- // Sends an HTML response containing the provided data with optional headers.
388
- // The response is base64 encoded, and the Content-Type header is set to text/html.
389
-
390
- return res.status301(url, headers);
391
- // Redirects the client to the specified url with a 301 status code and optional headers.
392
- // Useful for permanent redirections.
393
-
394
- return res.status404(data, headers);
395
- // Sends a 404 Not Found response with custom data and optional headers.
396
- // The response is base64 encoded, and the Content-Type header is set to text/html.
397
-
398
- return res.cors();
399
- // Sends a 200 OK response with CORS headers enabled.
400
- // This is typically used in response to a preflight request in a CORS scenario.
401
-
402
- return res.fileBase64(fileBase64, mimeType, headers);
403
- // Sends a file response with the content provided in base64 format,
404
- // the specified mimeType, and optional headers.
405
-
406
- return res.file(filePath, headers, fallbackFilePath);
407
- // Serves a file from the server's public directory, with optional headers.
408
- // If the file is not found and a fallbackFilePath is provided,
409
- // attempts to serve the fallback file.
410
- // Returns a JSON error if neither file is found.
411
-
412
- return res.ejsFile(filePath, pageData, headers);
413
- // Renders and serves an ejs file from the server's ejs directory, with optional headers.
414
- // Returns a JSON error if file is not found.
415
-
416
- return res.ejsTemplate(template, pageData, headers);
417
- // Renders and serves an ejs template string, with optional headers.
418
-
419
- return res.api(payload, { notAuthorized, message, errorMessage }, headers);
420
- // This function works together with the LambderCaller from the frontend.
421
- // Sends a standardized API response including the payload and status
422
- // flags like versionExpired, sessionExpired, notAuthorized, along with
423
- // optional messages and headers.
424
-
425
- // res.die.*
426
- // Acts the same as res.* but will:
427
- // - Immediately return the value.
428
- // - Skip the afterRender hooks.
429
-
430
- return res.die.raw(param);
431
- return res.die.json(data, headers);
432
- return res.die.xml(data);
433
- return res.die.html(data, headers);
434
- return res.die.status301(url, headers);
435
- return res.die.status404(data, headers);
436
- return res.die.cors();
437
- return res.die.fileBase64(fileBase64, mimeType, headers);
438
- return await res.die.file(filePath, headers, fallbackFilePath);
439
- return await res.die.ejsFile(filePath, pageData, headers);
440
- return await res.die.ejsTemplate(template, pageData, headers);
441
- return res.die.api(payload, { versionExpired, sessionExpired, notAuthorized, message, errorMessage }, headers);
442
- });
443
- ```
384
+ return res.status404(data, headers);
385
+ // Sends a 404 Not Found response.
444
386
 
445
- ## Frontend Usage with LambderCaller
387
+ return res.cors();
388
+ // Sends a 200 OK response with CORS headers (for preflight requests).
446
389
 
447
- LambderCaller is a frontend companion library for Lambder, it is only 2kb compressed, and designed to simplify making API requests to your Lambder backend services.
390
+ return res.fileBase64(fileBase64, mimeType, headers);
391
+ // Sends a file response from base64 content.
448
392
 
449
- ### Installing LambderCaller
393
+ return res.file(filePath, headers, fallbackFilePath);
394
+ // Serves a file from the public directory.
450
395
 
451
- LambderCaller is included in the same `lambder` package. Ensure you have `lambder` available in your frontend project.
396
+ return await res.ejsFile(filePath, pageData, headers);
397
+ // Renders and serves an EJS file.
452
398
 
453
- ### Basic Setup
399
+ return await res.ejsTemplate(template, pageData, headers);
400
+ // Renders and serves an EJS template string.
401
+
402
+ return res.api(payload, config, headers);
403
+ // Sends a standardized API response for use with LambderCaller.
404
+ // Config: { notAuthorized, message, errorMessage, versionExpired, sessionExpired }
454
405
 
455
- Begin by initializing LambderCaller with your API configuration. This setup assumes your project structure accommodates a place for initiating and configuring API handlers, possibly within a dedicated JavaScript module or directly in your main application file.
406
+ // res.die.* - Same as res.* but immediately returns and skips afterRender hooks
407
+ return res.die.json(data, headers);
408
+ return res.die.api(payload, config, headers);
409
+ // ... etc
410
+ ```
456
411
 
457
- ```javascript
412
+ ## Frontend Usage with LambderCaller
413
+
414
+ LambderCaller is a frontend companion library for Lambder (only 2kb compressed) designed to simplify making type-safe API requests to your Lambder backend.
415
+
416
+ ### Basic Setup with Type Safety
417
+
418
+ ```typescript
458
419
  import { LambderCaller } from "lambder";
420
+ import type { AppContract } from "./backend/handler"; // Import the inferred contract type
459
421
 
460
- const lambderCaller = new LambderCaller({
422
+ const lambderCaller = new LambderCaller<AppContract>({
423
+ apiPath: "/api",
461
424
  isCorsEnabled: false,
462
- apiPath: "/secure", // Your Lambder API endpoint, must be the same as in your backend
463
425
  fetchStartedHandler: ({ fetchParams, activeFetchList }) => {
464
- // When any api call starts
465
426
  console.log("API Called:", fetchParams.apiName);
466
427
  },
467
428
  fetchEndedHandler: ({ fetchParams, fetchResult, activeFetchList }) => {
468
- // When any api call ends
469
- console.log("Ongoing api call count:", activeFetchList.length);
429
+ console.log("Ongoing calls:", activeFetchList.length);
470
430
  },
471
431
  errorMessageHandler: (message) => {
472
- console.error("LambderCaller:", message); // Handle error messages
432
+ console.error("LambderCaller:", message);
473
433
  },
474
434
  });
475
435
 
476
-
477
- const loadPageData = async () => {
478
- try {
479
- const response = await lambderCaller.api("getCompanyPage", {
480
- companyName: "example", // Pass necessary parameters for your API call
481
- });
482
- } catch (error) {
483
- console.error("Failed to fetch page data:", error);
484
- }
485
- };
436
+ // Fully typed API calls!
437
+ const user = await lambderCaller.api("getCompanyPage", { companyName: "Acme" });
438
+ // TypeScript knows:
439
+ // - Available API names (autocomplete)
440
+ // - Required input type
441
+ // - Expected output type
486
442
  ```
487
443
 
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 = ApiContract<{
499
- getUserById: { input: { userId: string }, output: User },
500
- createUser: { input: CreateUserInput, output: User },
501
- listUsers: { input: void, output: User[] }
502
- }>;
503
- ```
504
-
505
- ### 2. Backend - Pass Type to Constructor
444
+ ### How Type Safety Works
506
445
 
446
+ 1. **Backend**: Chain your APIs and export the inferred contract
507
447
  ```typescript
448
+ // backend/handler.ts
508
449
  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
- });
450
+ import { z } from 'zod';
451
+
452
+ const lambder = new Lambder({ apiPath: '/api' })
453
+ .addApi('getUser', {
454
+ input: z.object({ userId: z.string() }),
455
+ output: z.object({ id: z.string(), name: z.string() })
456
+ }, async (ctx, res) => {
457
+ return res.api({ id: ctx.apiPayload.userId, name: "John" });
458
+ });
459
+
460
+ export type AppContract = typeof lambder.ApiContractType;
461
+ export const handler = lambder.getHandler();
519
462
  ```
520
463
 
521
- ### 3. Frontend - Pass Type to Constructor
522
-
464
+ 2. **Frontend**: Import the **type** (not the code) and use it
523
465
  ```typescript
466
+ // frontend/api.ts
524
467
  import { LambderCaller } from 'lambder';
525
- import type { MyApiContract } from './shared/apiContract';
468
+ import type { AppContract } from '../backend/handler'; // Type-only import
526
469
 
527
- const caller = new LambderCaller<MyApiContract>({ apiPath: '/api', isCorsEnabled: false });
470
+ const lambderCaller = new LambderCaller<AppContract>({ apiPath: '/api' });
528
471
 
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
472
+ // Fully typed - TypeScript knows all APIs and their input/output types
473
+ const user = await lambderCaller.api('getUser', { userId: '123' });
534
474
  ```
535
475
 
536
476
  ### Benefits
537
477
 
538
- ✅ **Simple** - Just pass type to constructor, that's it!
478
+ ✅ **No Manual Type Definitions** - Types are inferred from your Zod schemas
479
+ ✅ **Single Source of Truth** - API contract comes from your backend code
480
+ ✅ **Runtime Validation** - Zod validates inputs automatically
481
+ ✅ **Compile-Time Safety** - TypeScript catches errors before runtime
539
482
  ✅ **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
483
+ ✅ **Zero Overhead** - Type-only imports, no runtime code bloat
544
484
 
545
485
  📖 **[Read the Quick Start Guide](docs/TYPE_SAFE_QUICK_START.md)** for more details and examples!
546
486
 
547
487
  ## Testing with LambderMSW
548
488
 
549
- LambderMSW provides seamless integration with [MSW (Mock Service Worker)](https://mswjs.io/) for testing your APIs. It works perfectly with type-safe API contracts!
489
+ LambderMSW provides seamless integration with [MSW (Mock Service Worker)](https://mswjs.io/) for testing your APIs with full type safety.
550
490
 
551
491
  ```typescript
552
492
  import { LambderMSW } from 'lambder';
553
493
  import { setupServer } from 'msw/node';
554
- import type { MyApiContract } from './shared/apiContract';
494
+ import type { AppContract } from './backend/handler';
555
495
 
556
- const lambderMSW = new LambderMSW<MyApiContract>({
496
+ const lambderMSW = new LambderMSW<AppContract>({
557
497
  apiPath: '/api',
558
498
  });
559
499
 
560
500
  const handlers = [
561
501
  // Mock API with full type safety! ✨
562
- lambderMSW.mockApi('getUserById', async (payload) => {
502
+ lambderMSW.mockApi('getUser', async (payload) => {
503
+ // payload is typed based on your Zod schema
563
504
  return {
564
505
  id: payload.userId,
565
506
  name: 'John Doe',
@@ -567,11 +508,15 @@ const handlers = [
567
508
  };
568
509
  }),
569
510
 
570
- // Mock errors, delays, and more
511
+ // Simulate delays and custom responses
571
512
  lambderMSW.mockApi('createUser', async (payload) => {
572
- return { id: '123', ...payload };
573
- }, { delay: 500 }),
513
+ return { id: '123', name: payload.name, email: payload.email };
514
+ }, {
515
+ delay: 500,
516
+ message: 'User created successfully'
517
+ }),
574
518
 
519
+ // Mock session expired
575
520
  lambderMSW.mockSessionExpired('protectedApi'),
576
521
  ];
577
522