lambder 1.0.125 → 1.0.127
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 +59 -0
- package/deploy +5 -0
- package/dist/Lambder.d.ts +20 -15
- package/dist/Lambder.js +2 -0
- package/dist/LambderApiContract.d.ts +53 -0
- package/dist/LambderApiContract.js +26 -0
- package/dist/LambderCaller.d.ts +15 -4
- package/dist/LambderCaller.js +2 -2
- package/dist/LambderResolver.d.ts +1 -1
- package/dist/LambderResponseBuilder.d.ts +1 -1
- package/dist/LambderSessionController.d.ts +2 -2
- 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 +5 -3
- package/src/Lambder.ts +63 -24
- package/src/LambderApiContract.ts +63 -0
- package/src/LambderCaller.ts +40 -15
- package/src/LambderResolver.ts +1 -1
- package/src/LambderResponseBuilder.ts +2 -2
- package/src/LambderSessionController.ts +2 -2
- package/src/index.ts +7 -0
- package/tests/type-safety.test.ts +202 -0
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 = 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
|
|
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/deploy
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
# Exit immediately if a command exits with a non-zero status.
|
|
4
4
|
set -e
|
|
5
5
|
|
|
6
|
+
echo "Running tests..."
|
|
7
|
+
npm run test
|
|
8
|
+
|
|
9
|
+
echo "Tests passed! Proceeding with deployment..."
|
|
10
|
+
|
|
6
11
|
# Update the version, build the project, and publish
|
|
7
12
|
rm -rf ./dist/*
|
|
8
13
|
npm version patch --no-git-tag-version
|
package/dist/Lambder.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ 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 { ApiContractShape } from "./LambderApiContract.js";
|
|
7
8
|
type Path = `/${string}`;
|
|
8
|
-
export type LambderRenderContext = {
|
|
9
|
+
export type LambderRenderContext<TApiPayload = any> = {
|
|
9
10
|
host: string;
|
|
10
11
|
path: string;
|
|
11
12
|
pathParams: Record<string, any> | null;
|
|
@@ -14,7 +15,7 @@ export type LambderRenderContext = {
|
|
|
14
15
|
post: Record<string, any>;
|
|
15
16
|
cookie: Record<string, any>;
|
|
16
17
|
apiName: string;
|
|
17
|
-
apiPayload:
|
|
18
|
+
apiPayload: TApiPayload;
|
|
18
19
|
headers: APIGatewayProxyEventHeaders;
|
|
19
20
|
session: LambderSessionContext | null;
|
|
20
21
|
event: APIGatewayProxyEvent;
|
|
@@ -33,17 +34,17 @@ export type LambderRenderContext = {
|
|
|
33
34
|
};
|
|
34
35
|
};
|
|
35
36
|
type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
|
|
36
|
-
type ConditionFunction = (ctx: LambderRenderContext) => boolean;
|
|
37
|
-
type ActionFunction = (ctx: LambderRenderContext
|
|
37
|
+
type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
|
|
38
|
+
type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
38
39
|
type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
|
|
39
|
-
type HookBeforeRenderFunction = (ctx: LambderRenderContext
|
|
40
|
-
type HookAfterRenderFunction = (ctx: LambderRenderContext
|
|
41
|
-
type HookFallbackFunction = (ctx: LambderRenderContext
|
|
42
|
-
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext | null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
43
|
-
type RouteFallbackHandlerFunction = (ctx: LambderRenderContext
|
|
44
|
-
type ApiFallbackHandlerFunction = (ctx: LambderRenderContext
|
|
45
|
-
export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext
|
|
46
|
-
export default class Lambder {
|
|
40
|
+
type HookBeforeRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderRenderContext<any> | Error | Promise<LambderRenderContext<any> | Error>;
|
|
41
|
+
type HookAfterRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver, response: LambderResolverResponse) => LambderResolverResponse | Error | Promise<LambderResolverResponse | Error>;
|
|
42
|
+
type HookFallbackFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => void | Promise<void>;
|
|
43
|
+
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext<any> | null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
44
|
+
type RouteFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
45
|
+
type ApiFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
46
|
+
export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext<any>;
|
|
47
|
+
export default class Lambder<TContract extends ApiContractShape = any> {
|
|
47
48
|
apiPath: string;
|
|
48
49
|
apiVersion: null | string;
|
|
49
50
|
isCorsEnabled: boolean;
|
|
@@ -86,13 +87,17 @@ 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:
|
|
90
|
-
|
|
90
|
+
addApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
91
|
+
addApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
|
|
92
|
+
addApi(apiName: string, actionFn: ActionFunction): void;
|
|
93
|
+
addSessionApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
94
|
+
addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
|
|
95
|
+
addSessionApi(apiName: string, actionFn: ActionFunction): void;
|
|
91
96
|
addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
92
97
|
addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
93
98
|
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
94
99
|
addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
|
|
95
|
-
getSessionController(ctx: LambderRenderContext): LambderSessionController;
|
|
100
|
+
getSessionController(ctx: LambderRenderContext<any>): LambderSessionController;
|
|
96
101
|
getResponseBuilder(): LambderResponseBuilder;
|
|
97
102
|
private getResolver;
|
|
98
103
|
render(event: APIGatewayProxyEvent, lambdaContext: Context): Promise<LambderResolverResponse>;
|
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,53 @@
|
|
|
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
|
+
* Use this as a constraint when defining your API contract:
|
|
30
|
+
*
|
|
31
|
+
* export type MyApiContract = {
|
|
32
|
+
* echo: { input: { message: string }, output: { echo: string } }
|
|
33
|
+
* } satisfies ApiContract;
|
|
34
|
+
*
|
|
35
|
+
* Or for backward compatibility without satisfies:
|
|
36
|
+
*
|
|
37
|
+
* export type MyApiContract = ApiContract & {
|
|
38
|
+
* echo: { input: { message: string }, output: { echo: string } }
|
|
39
|
+
* }
|
|
40
|
+
*/
|
|
41
|
+
export type ApiContractShape = Record<string, {
|
|
42
|
+
input: any;
|
|
43
|
+
output: any;
|
|
44
|
+
}>;
|
|
45
|
+
export type ApiContract<T extends ApiContractShape> = T;
|
|
46
|
+
/**
|
|
47
|
+
* Extract input type from contract for a specific API
|
|
48
|
+
*/
|
|
49
|
+
export type ApiInput<TContract extends ApiContractShape, TApiName extends keyof TContract> = TContract[TApiName]['input'];
|
|
50
|
+
/**
|
|
51
|
+
* Extract output type from contract for a specific API
|
|
52
|
+
*/
|
|
53
|
+
export type ApiOutput<TContract extends ApiContractShape, 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 { ApiContractShape } 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 ApiContractShape = 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
|
@@ -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(
|
|
135
|
-
const result = await this.apiRaw(
|
|
134
|
+
async api(apiName, payload, options) {
|
|
135
|
+
const result = await this.apiRaw(apiName, payload, options);
|
|
136
136
|
return result?.payload;
|
|
137
137
|
}
|
|
138
138
|
}
|
|
@@ -25,7 +25,7 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
25
25
|
publicPath: string;
|
|
26
26
|
apiVersion?: string | null;
|
|
27
27
|
lambderUtils: LambderUtils;
|
|
28
|
-
ctx: LambderRenderContext
|
|
28
|
+
ctx: LambderRenderContext<any>;
|
|
29
29
|
resolve: (response: LambderResolverResponse) => void;
|
|
30
30
|
reject: (err: Error) => void;
|
|
31
31
|
});
|
|
@@ -28,7 +28,7 @@ export default class LambderResponseBuilder {
|
|
|
28
28
|
publicPath: string;
|
|
29
29
|
apiVersion?: string | null;
|
|
30
30
|
lambderUtils: LambderUtils;
|
|
31
|
-
ctx?: LambderRenderContext
|
|
31
|
+
ctx?: LambderRenderContext<any>;
|
|
32
32
|
});
|
|
33
33
|
private readPublicFileSync;
|
|
34
34
|
private checkPublicFileExist;
|
|
@@ -5,12 +5,12 @@ export default class LambderSessionController {
|
|
|
5
5
|
lambderSessionManager: LambderSessionManager;
|
|
6
6
|
sessionTokenCookieKey: string;
|
|
7
7
|
sessionCsrfCookieKey: string;
|
|
8
|
-
ctx: LambderRenderContext
|
|
8
|
+
ctx: LambderRenderContext<any>;
|
|
9
9
|
constructor({ lambderSessionManager, sessionTokenCookieKey, sessionCsrfCookieKey, ctx, }: {
|
|
10
10
|
lambderSessionManager: LambderSessionManager;
|
|
11
11
|
sessionTokenCookieKey: string;
|
|
12
12
|
sessionCsrfCookieKey: string;
|
|
13
|
-
ctx: LambderRenderContext
|
|
13
|
+
ctx: LambderRenderContext<any>;
|
|
14
14
|
});
|
|
15
15
|
private areRequestSessionTokensValid;
|
|
16
16
|
createSession(sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
|
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 = ApiContract<{
|
|
12
|
+
getUserById: { input: { userId: string }, output: User },
|
|
13
|
+
createUser: { input: CreateUserInput, output: User },
|
|
14
|
+
listUsers: { input: void, output: User[] }
|
|
15
|
+
}>;
|
|
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
|