lambder 1.0.124 → 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 +60 -1
- package/dist/Lambder.d.ts +14 -4
- package/dist/Lambder.js +4 -2
- package/dist/LambderApiContract.d.ts +42 -0
- package/dist/LambderApiContract.js +26 -0
- package/dist/LambderCaller.d.ts +15 -4
- package/dist/LambderCaller.js +11 -5
- package/dist/LambderResponseBuilder.d.ts +1 -0
- package/dist/LambderResponseBuilder.js +32 -1
- package/dist/index.d.ts +1 -0
- package/docs/TYPE_SAFE_QUICK_START.md +123 -0
- package/examples/simplified-typed-api-example.ts +365 -0
- package/package.json +1 -1
- package/src/Lambder.ts +43 -3
- package/src/LambderApiContract.ts +51 -0
- package/src/LambderCaller.ts +48 -18
- package/src/LambderResponseBuilder.ts +40 -1
- package/src/index.ts +7 -0
- package/tests/type-safety.test.ts +224 -0
package/Readme.md
CHANGED
|
@@ -43,7 +43,7 @@ lambder.enableDdbSession({
|
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
// Enable Cors
|
|
46
|
-
lambder.
|
|
46
|
+
lambder.enableCors(true);
|
|
47
47
|
|
|
48
48
|
// Define a simple api
|
|
49
49
|
lambder.addApi("getCompanyPage", async ({ apiPayload }, res) => {
|
|
@@ -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;
|
|
@@ -17,6 +18,7 @@ export type LambderRenderContext = {
|
|
|
17
18
|
apiPayload: any;
|
|
18
19
|
headers: APIGatewayProxyEventHeaders;
|
|
19
20
|
session: LambderSessionContext | null;
|
|
21
|
+
event: APIGatewayProxyEvent;
|
|
20
22
|
lambdaContext: Context;
|
|
21
23
|
_otherInternal: {
|
|
22
24
|
isApiCall: boolean;
|
|
@@ -42,7 +44,7 @@ type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext | null,
|
|
|
42
44
|
type RouteFallbackHandlerFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
|
|
43
45
|
type ApiFallbackHandlerFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
|
|
44
46
|
export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext;
|
|
45
|
-
export default class Lambder {
|
|
47
|
+
export default class Lambder<TContract extends ApiContract = any> {
|
|
46
48
|
apiPath: string;
|
|
47
49
|
apiVersion: null | string;
|
|
48
50
|
isCorsEnabled: boolean;
|
|
@@ -63,7 +65,7 @@ export default class Lambder {
|
|
|
63
65
|
ejsPath?: string;
|
|
64
66
|
apiVersion?: string;
|
|
65
67
|
});
|
|
66
|
-
|
|
68
|
+
enableCors(isCorsEnabled: boolean): void;
|
|
67
69
|
enableDdbSession({ tableName, tableRegion, sessionSalt }: {
|
|
68
70
|
tableName: string;
|
|
69
71
|
tableRegion: string;
|
|
@@ -85,8 +87,16 @@ export default class Lambder {
|
|
|
85
87
|
}>): Promise<void>;
|
|
86
88
|
addRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
87
89
|
addSessionRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
88
|
-
addApi(apiName:
|
|
89
|
-
|
|
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;
|
|
90
100
|
addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
91
101
|
addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
92
102
|
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
package/dist/Lambder.js
CHANGED
|
@@ -33,7 +33,7 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
33
33
|
const apiPayload = isApiCall ? post.payload : null;
|
|
34
34
|
return {
|
|
35
35
|
host, path, pathParams, method,
|
|
36
|
-
get, post, cookie,
|
|
36
|
+
get, post, cookie, event,
|
|
37
37
|
apiName, apiPayload,
|
|
38
38
|
headers, session, lambdaContext,
|
|
39
39
|
_otherInternal: {
|
|
@@ -72,7 +72,7 @@ export default class Lambder {
|
|
|
72
72
|
};
|
|
73
73
|
this.utils = new LambderUtils({ ejsPath });
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
enableCors(isCorsEnabled) {
|
|
76
76
|
this.isCorsEnabled = isCorsEnabled;
|
|
77
77
|
}
|
|
78
78
|
enableDdbSession({ tableName, tableRegion, sessionSalt }, { partitionKey, sortKey } = { partitionKey: "pk", sortKey: "sk" }) {
|
|
@@ -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 {};
|
package/dist/LambderCaller.d.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
65
|
-
api<
|
|
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 {};
|
package/dist/LambderCaller.js
CHANGED
|
@@ -51,10 +51,16 @@ export default class LambderCaller {
|
|
|
51
51
|
credentials: 'same-origin', redirect: 'follow', referrerPolicy: 'origin',
|
|
52
52
|
headers: { 'Content-Type': 'application/json', ...(headers || {}) },
|
|
53
53
|
body: JSON.stringify({ apiName, version, token, siteHost, payload, }),
|
|
54
|
-
}).then(res => {
|
|
54
|
+
}).then(async (res) => {
|
|
55
55
|
if (res.status >= 500)
|
|
56
56
|
throw new Error("Request failed: " + res.status + " - " + res.statusText);
|
|
57
|
-
|
|
57
|
+
if (res.headers.get("Content-Type")?.includes("application/lambder-json-stream")) {
|
|
58
|
+
const decompressed = res.json();
|
|
59
|
+
return decompressed;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
58
64
|
});
|
|
59
65
|
fetchTracker.done = true;
|
|
60
66
|
if (this.fetchEndedHandler) {
|
|
@@ -95,7 +101,7 @@ export default class LambderCaller {
|
|
|
95
101
|
await this.notAuthorizedHandler();
|
|
96
102
|
}
|
|
97
103
|
else if (this.errorHandler) {
|
|
98
|
-
await this.errorHandler(new Error("
|
|
104
|
+
await this.errorHandler(new Error("Not Authorized;"));
|
|
99
105
|
}
|
|
100
106
|
return null;
|
|
101
107
|
}
|
|
@@ -125,8 +131,8 @@ export default class LambderCaller {
|
|
|
125
131
|
}
|
|
126
132
|
;
|
|
127
133
|
// Use the same type for api but adjust the return type
|
|
128
|
-
async api(
|
|
129
|
-
const result = await this.apiRaw(
|
|
134
|
+
async api(apiName, payload, options) {
|
|
135
|
+
const result = await this.apiRaw(apiName, payload, options);
|
|
130
136
|
return result?.payload;
|
|
131
137
|
}
|
|
132
138
|
}
|
|
@@ -47,4 +47,5 @@ export default class LambderResponseBuilder {
|
|
|
47
47
|
ejsTemplate(template: string, pageData: Record<string, any>, headers?: Record<string, string | string[]>): Promise<LambderResolverResponse>;
|
|
48
48
|
ejsFile(filePath: string, pageData: Record<string, any>, headers?: Record<string, string | string[]>): Promise<LambderResolverResponse>;
|
|
49
49
|
api<T = any>(payload: T | null, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, }?: LambderApiResponseConfig, headers?: Record<string, string | string[]>): LambderResolverResponse;
|
|
50
|
+
private apiBinary;
|
|
50
51
|
}
|
|
@@ -144,7 +144,11 @@ export default class LambderResponseBuilder {
|
|
|
144
144
|
}
|
|
145
145
|
const mimeType = mimeTypeResolver.lookup(filePath);
|
|
146
146
|
const body = this.readPublicFileSync(filePath);
|
|
147
|
-
|
|
147
|
+
if (body === "forbidden-public-path") {
|
|
148
|
+
throw { error: "Forbidden public path: " + filePath };
|
|
149
|
+
}
|
|
150
|
+
const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
151
|
+
const bodyBase64 = bodyBuffer.toString("base64");
|
|
148
152
|
console.log("bodyBase64.length", bodyBase64.length);
|
|
149
153
|
return this.fileBase64(bodyBase64, mimeType || "", headers);
|
|
150
154
|
}
|
|
@@ -186,5 +190,32 @@ export default class LambderResponseBuilder {
|
|
|
186
190
|
}, headers);
|
|
187
191
|
}
|
|
188
192
|
;
|
|
193
|
+
apiBinary(payload, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, } = {
|
|
194
|
+
versionExpired: undefined, sessionExpired: undefined, notAuthorized: undefined,
|
|
195
|
+
message: null, errorMessage: null, logList: undefined
|
|
196
|
+
}, headers) {
|
|
197
|
+
const finalLogList = logList || this.ctx?._otherInternal?.logToApiResponseAccumulator;
|
|
198
|
+
const result = {
|
|
199
|
+
apiVersion: this.apiVersion,
|
|
200
|
+
payload,
|
|
201
|
+
...(versionExpired ? { versionExpired } : {}),
|
|
202
|
+
...(sessionExpired ? { sessionExpired } : {}),
|
|
203
|
+
...(notAuthorized ? { notAuthorized } : {}),
|
|
204
|
+
...(message ? { message } : {}),
|
|
205
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
206
|
+
...(finalLogList?.length ? { logList: finalLogList } : {}),
|
|
207
|
+
};
|
|
208
|
+
return this.raw({
|
|
209
|
+
statusCode: 200,
|
|
210
|
+
isBase64Encoded: true,
|
|
211
|
+
multiValueHeaders: {
|
|
212
|
+
"Content-Type": ["application/lambder-json-stream"],
|
|
213
|
+
"Content-Encoding": ["gzip"],
|
|
214
|
+
...convertToMultiHeader(headers)
|
|
215
|
+
},
|
|
216
|
+
body: Buffer.from(JSON.stringify(result)).toString("base64"),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
;
|
|
189
220
|
}
|
|
190
221
|
;
|
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
|