elysia-lambda-handler 0.1.0

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 ADDED
@@ -0,0 +1,91 @@
1
+ # elysia-lambda-handler
2
+
3
+ Run [Elysia](https://elysiajs.com/) apps on AWS Lambda with the **Node.js runtime**.
4
+
5
+ This handler converts an API Gateway REST API (payload v1) proxy event into a Web
6
+ standard `Request`, passes it to `app.handle()`, and converts the returned
7
+ `Response` back into the Lambda proxy result format. No HTTP server is started
8
+ and no Bun-specific APIs are used — only the Web APIs built into Node.js 18+.
9
+
10
+ - Zero runtime dependencies (`elysia` is a peer dependency)
11
+ - Works with `NodejsFunction` / esbuild bundling out of the box
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ npm install elysia-lambda-handler
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Keep your Elysia app runtime-agnostic:
22
+
23
+ ```ts
24
+ // app.ts
25
+ import { Elysia } from "elysia";
26
+
27
+ export const app = new Elysia().get("/", () => ({ hello: "lambda" }));
28
+ ```
29
+
30
+ Then wrap it once at module scope in your Lambda entry:
31
+
32
+ ```ts
33
+ // lambda.ts
34
+ import { handle } from "elysia-lambda-handler";
35
+ import { app } from "./app.js";
36
+
37
+ export const handler = handle(app);
38
+ ```
39
+
40
+ Deploy `lambda.ts` behind an API Gateway REST API with Lambda proxy
41
+ integration (for example CDK's `LambdaRestApi`).
42
+
43
+ ## Supported event types
44
+
45
+ | Event source | Status |
46
+ | --- | --- |
47
+ | API Gateway REST API (payload v1), proxy integration | ✅ Supported |
48
+ | API Gateway HTTP API (payload v2) | Planned |
49
+ | Lambda Function URL | Planned |
50
+
51
+ ## Binary responses
52
+
53
+ When the `Response` has a non-text `content-type` (anything other than
54
+ `text/*`, `application/json`, `+json` / `+xml` suffixes, or a type with an
55
+ explicit `charset`), the body is returned base64-encoded with
56
+ `isBase64Encoded: true`. Note that a REST API only decodes it back to bytes
57
+ when the API's `binaryMediaTypes` matches the request — configure it (for
58
+ example `*/*`) if you serve images or other binary content.
59
+
60
+ Multiple `Set-Cookie` headers are returned via `multiValueHeaders`, which the
61
+ REST API supports natively.
62
+
63
+ ## Cold starts
64
+
65
+ On Lambda, cold-start time usually dominates. Two tips:
66
+
67
+ - Call `handle(app)` at module scope (as in the example above) so the app and
68
+ its route compilation are initialized once per execution environment, not per
69
+ invocation.
70
+ - Elysia's ahead-of-time route compilation (`aot`, enabled by default) makes
71
+ the *first* request in each execution environment more expensive when the app
72
+ has many routes. If cold-start latency matters more than steady-state
73
+ throughput — the common case for Lambda — try `new Elysia({ aot: false })`
74
+ and measure both variants for your workload.
75
+
76
+ ## API
77
+
78
+ ### `handle(app)`
79
+
80
+ Takes anything with a fetch-style `handle(request: Request)` method (an Elysia
81
+ instance) and returns an async Lambda handler
82
+ `(event, context?) => Promise<APIGatewayProxyResult>`.
83
+
84
+ The lower-level conversion helpers `eventToRequest(event)` and
85
+ `responseToResult(response)` are also exported, along with the structural
86
+ types `APIGatewayProxyEvent` / `APIGatewayProxyResult` (compatible with
87
+ `@types/aws-lambda`, which is not required at runtime or compile time).
88
+
89
+ ## License
90
+
91
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,90 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/request.ts
3
+ const findHeader = (event, name) => {
4
+ for (const [key, value] of Object.entries(event.headers ?? {})) if (key.toLowerCase() === name && value !== void 0) return value;
5
+ for (const [key, values] of Object.entries(event.multiValueHeaders ?? {})) if (key.toLowerCase() === name && values?.length) return values[0];
6
+ };
7
+ const buildSearch = (event) => {
8
+ const params = new URLSearchParams();
9
+ if (event.multiValueQueryStringParameters) for (const [key, values] of Object.entries(event.multiValueQueryStringParameters)) for (const value of values ?? []) params.append(key, value);
10
+ else if (event.queryStringParameters) {
11
+ for (const [key, value] of Object.entries(event.queryStringParameters)) if (value !== void 0) params.append(key, value);
12
+ }
13
+ const search = params.toString();
14
+ return search ? `?${search}` : "";
15
+ };
16
+ const buildHeaders = (event) => {
17
+ const headers = new Headers();
18
+ if (event.multiValueHeaders) for (const [key, values] of Object.entries(event.multiValueHeaders)) for (const value of values ?? []) headers.append(key, value);
19
+ else if (event.headers) {
20
+ for (const [key, value] of Object.entries(event.headers)) if (value !== void 0) headers.append(key, value);
21
+ }
22
+ return headers;
23
+ };
24
+ /**
25
+ * Convert an API Gateway REST API (payload v1) proxy event into a Web
26
+ * standard `Request` that can be passed to `app.handle()`.
27
+ */
28
+ const eventToRequest = (event) => {
29
+ const host = findHeader(event, "host") ?? event.requestContext?.domainName ?? "localhost";
30
+ const method = event.httpMethod.toUpperCase();
31
+ const url = `https://${host}${event.path}${buildSearch(event)}`;
32
+ let body;
33
+ if (event.body !== null && method !== "GET" && method !== "HEAD") body = Buffer.from(event.body, event.isBase64Encoded ? "base64" : "utf8");
34
+ return new Request(url, {
35
+ method,
36
+ headers: buildHeaders(event),
37
+ body
38
+ });
39
+ };
40
+ //#endregion
41
+ //#region src/response.ts
42
+ const TEXT_TYPE_PATTERN = /^text\/|^application\/(json|javascript|xml|x-www-form-urlencoded)\b|[+](json|xml)\b|charset=/i;
43
+ const isTextContentType = (contentType) => contentType === null || TEXT_TYPE_PATTERN.test(contentType);
44
+ /**
45
+ * Convert a Web standard `Response` returned by `app.handle()` into an API
46
+ * Gateway REST API (payload v1) proxy result. Text bodies are passed through
47
+ * as UTF-8; anything else is base64-encoded with `isBase64Encoded: true`
48
+ * (the REST API additionally requires matching `binaryMediaTypes` to decode
49
+ * it on the way out).
50
+ */
51
+ const responseToResult = async (response) => {
52
+ const multiValueHeaders = {};
53
+ for (const [key, value] of response.headers) {
54
+ if (key === "set-cookie") continue;
55
+ multiValueHeaders[key] = [value];
56
+ }
57
+ const setCookie = response.headers.getSetCookie();
58
+ if (setCookie.length > 0) multiValueHeaders["set-cookie"] = setCookie;
59
+ const buffer = Buffer.from(await response.arrayBuffer());
60
+ const isText = isTextContentType(response.headers.get("content-type"));
61
+ return {
62
+ statusCode: response.status,
63
+ multiValueHeaders,
64
+ body: buffer.toString(isText ? "utf8" : "base64"),
65
+ isBase64Encoded: !isText
66
+ };
67
+ };
68
+ //#endregion
69
+ //#region src/index.ts
70
+ /**
71
+ * Wrap an Elysia app as an AWS Lambda handler for API Gateway REST API
72
+ * (payload v1) proxy integration events.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { handle } from "elysia-lambda-handler";
77
+ * import { app } from "./app.js";
78
+ *
79
+ * export const handler = handle(app);
80
+ * ```
81
+ */
82
+ const handle = (app) => {
83
+ return async (event) => {
84
+ return responseToResult(await app.handle(eventToRequest(event)));
85
+ };
86
+ };
87
+ //#endregion
88
+ exports.eventToRequest = eventToRequest;
89
+ exports.handle = handle;
90
+ exports.responseToResult = responseToResult;
@@ -0,0 +1,74 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * Structurally-compatible subset of the API Gateway REST API (payload v1)
4
+ * proxy event. Kept local so this package has no runtime or type
5
+ * dependencies — an `APIGatewayProxyEvent` from `@types/aws-lambda` is
6
+ * assignable to this type.
7
+ */
8
+ interface APIGatewayProxyEvent {
9
+ httpMethod: string;
10
+ path: string;
11
+ headers: Record<string, string | undefined> | null;
12
+ multiValueHeaders: Record<string, string[] | undefined> | null;
13
+ queryStringParameters: Record<string, string | undefined> | null;
14
+ multiValueQueryStringParameters: Record<string, string[] | undefined> | null;
15
+ body: string | null;
16
+ isBase64Encoded: boolean;
17
+ requestContext: {
18
+ domainName?: string;
19
+ };
20
+ }
21
+ /**
22
+ * API Gateway REST API (payload v1) proxy result. Structurally compatible
23
+ * with `APIGatewayProxyResult` from `@types/aws-lambda`.
24
+ */
25
+ interface APIGatewayProxyResult {
26
+ statusCode: number;
27
+ multiValueHeaders: Record<string, string[]>;
28
+ body: string;
29
+ isBase64Encoded: boolean;
30
+ }
31
+ /**
32
+ * The subset of an Elysia instance the handler relies on. Any object with a
33
+ * fetch-style `handle` method works, which keeps `elysia` a peer dependency
34
+ * used only at the type level by consumers.
35
+ */
36
+ interface HandleTarget {
37
+ handle: (request: Request) => Response | Promise<Response>;
38
+ }
39
+ /** Lambda handler signature returned by {@link handle}. */
40
+ type LambdaHandler = (event: APIGatewayProxyEvent, context?: unknown) => Promise<APIGatewayProxyResult>;
41
+ //#endregion
42
+ //#region src/request.d.ts
43
+ /**
44
+ * Convert an API Gateway REST API (payload v1) proxy event into a Web
45
+ * standard `Request` that can be passed to `app.handle()`.
46
+ */
47
+ declare const eventToRequest: (event: APIGatewayProxyEvent) => Request;
48
+ //#endregion
49
+ //#region src/response.d.ts
50
+ /**
51
+ * Convert a Web standard `Response` returned by `app.handle()` into an API
52
+ * Gateway REST API (payload v1) proxy result. Text bodies are passed through
53
+ * as UTF-8; anything else is base64-encoded with `isBase64Encoded: true`
54
+ * (the REST API additionally requires matching `binaryMediaTypes` to decode
55
+ * it on the way out).
56
+ */
57
+ declare const responseToResult: (response: Response) => Promise<APIGatewayProxyResult>;
58
+ //#endregion
59
+ //#region src/index.d.ts
60
+ /**
61
+ * Wrap an Elysia app as an AWS Lambda handler for API Gateway REST API
62
+ * (payload v1) proxy integration events.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * import { handle } from "elysia-lambda-handler";
67
+ * import { app } from "./app.js";
68
+ *
69
+ * export const handler = handle(app);
70
+ * ```
71
+ */
72
+ declare const handle: (app: HandleTarget) => LambdaHandler;
73
+ //#endregion
74
+ export { type APIGatewayProxyEvent, type APIGatewayProxyResult, type HandleTarget, type LambdaHandler, eventToRequest, handle, responseToResult };
@@ -0,0 +1,74 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * Structurally-compatible subset of the API Gateway REST API (payload v1)
4
+ * proxy event. Kept local so this package has no runtime or type
5
+ * dependencies — an `APIGatewayProxyEvent` from `@types/aws-lambda` is
6
+ * assignable to this type.
7
+ */
8
+ interface APIGatewayProxyEvent {
9
+ httpMethod: string;
10
+ path: string;
11
+ headers: Record<string, string | undefined> | null;
12
+ multiValueHeaders: Record<string, string[] | undefined> | null;
13
+ queryStringParameters: Record<string, string | undefined> | null;
14
+ multiValueQueryStringParameters: Record<string, string[] | undefined> | null;
15
+ body: string | null;
16
+ isBase64Encoded: boolean;
17
+ requestContext: {
18
+ domainName?: string;
19
+ };
20
+ }
21
+ /**
22
+ * API Gateway REST API (payload v1) proxy result. Structurally compatible
23
+ * with `APIGatewayProxyResult` from `@types/aws-lambda`.
24
+ */
25
+ interface APIGatewayProxyResult {
26
+ statusCode: number;
27
+ multiValueHeaders: Record<string, string[]>;
28
+ body: string;
29
+ isBase64Encoded: boolean;
30
+ }
31
+ /**
32
+ * The subset of an Elysia instance the handler relies on. Any object with a
33
+ * fetch-style `handle` method works, which keeps `elysia` a peer dependency
34
+ * used only at the type level by consumers.
35
+ */
36
+ interface HandleTarget {
37
+ handle: (request: Request) => Response | Promise<Response>;
38
+ }
39
+ /** Lambda handler signature returned by {@link handle}. */
40
+ type LambdaHandler = (event: APIGatewayProxyEvent, context?: unknown) => Promise<APIGatewayProxyResult>;
41
+ //#endregion
42
+ //#region src/request.d.ts
43
+ /**
44
+ * Convert an API Gateway REST API (payload v1) proxy event into a Web
45
+ * standard `Request` that can be passed to `app.handle()`.
46
+ */
47
+ declare const eventToRequest: (event: APIGatewayProxyEvent) => Request;
48
+ //#endregion
49
+ //#region src/response.d.ts
50
+ /**
51
+ * Convert a Web standard `Response` returned by `app.handle()` into an API
52
+ * Gateway REST API (payload v1) proxy result. Text bodies are passed through
53
+ * as UTF-8; anything else is base64-encoded with `isBase64Encoded: true`
54
+ * (the REST API additionally requires matching `binaryMediaTypes` to decode
55
+ * it on the way out).
56
+ */
57
+ declare const responseToResult: (response: Response) => Promise<APIGatewayProxyResult>;
58
+ //#endregion
59
+ //#region src/index.d.ts
60
+ /**
61
+ * Wrap an Elysia app as an AWS Lambda handler for API Gateway REST API
62
+ * (payload v1) proxy integration events.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * import { handle } from "elysia-lambda-handler";
67
+ * import { app } from "./app.js";
68
+ *
69
+ * export const handler = handle(app);
70
+ * ```
71
+ */
72
+ declare const handle: (app: HandleTarget) => LambdaHandler;
73
+ //#endregion
74
+ export { type APIGatewayProxyEvent, type APIGatewayProxyResult, type HandleTarget, type LambdaHandler, eventToRequest, handle, responseToResult };
package/dist/index.mjs ADDED
@@ -0,0 +1,87 @@
1
+ //#region src/request.ts
2
+ const findHeader = (event, name) => {
3
+ for (const [key, value] of Object.entries(event.headers ?? {})) if (key.toLowerCase() === name && value !== void 0) return value;
4
+ for (const [key, values] of Object.entries(event.multiValueHeaders ?? {})) if (key.toLowerCase() === name && values?.length) return values[0];
5
+ };
6
+ const buildSearch = (event) => {
7
+ const params = new URLSearchParams();
8
+ if (event.multiValueQueryStringParameters) for (const [key, values] of Object.entries(event.multiValueQueryStringParameters)) for (const value of values ?? []) params.append(key, value);
9
+ else if (event.queryStringParameters) {
10
+ for (const [key, value] of Object.entries(event.queryStringParameters)) if (value !== void 0) params.append(key, value);
11
+ }
12
+ const search = params.toString();
13
+ return search ? `?${search}` : "";
14
+ };
15
+ const buildHeaders = (event) => {
16
+ const headers = new Headers();
17
+ if (event.multiValueHeaders) for (const [key, values] of Object.entries(event.multiValueHeaders)) for (const value of values ?? []) headers.append(key, value);
18
+ else if (event.headers) {
19
+ for (const [key, value] of Object.entries(event.headers)) if (value !== void 0) headers.append(key, value);
20
+ }
21
+ return headers;
22
+ };
23
+ /**
24
+ * Convert an API Gateway REST API (payload v1) proxy event into a Web
25
+ * standard `Request` that can be passed to `app.handle()`.
26
+ */
27
+ const eventToRequest = (event) => {
28
+ const host = findHeader(event, "host") ?? event.requestContext?.domainName ?? "localhost";
29
+ const method = event.httpMethod.toUpperCase();
30
+ const url = `https://${host}${event.path}${buildSearch(event)}`;
31
+ let body;
32
+ if (event.body !== null && method !== "GET" && method !== "HEAD") body = Buffer.from(event.body, event.isBase64Encoded ? "base64" : "utf8");
33
+ return new Request(url, {
34
+ method,
35
+ headers: buildHeaders(event),
36
+ body
37
+ });
38
+ };
39
+ //#endregion
40
+ //#region src/response.ts
41
+ const TEXT_TYPE_PATTERN = /^text\/|^application\/(json|javascript|xml|x-www-form-urlencoded)\b|[+](json|xml)\b|charset=/i;
42
+ const isTextContentType = (contentType) => contentType === null || TEXT_TYPE_PATTERN.test(contentType);
43
+ /**
44
+ * Convert a Web standard `Response` returned by `app.handle()` into an API
45
+ * Gateway REST API (payload v1) proxy result. Text bodies are passed through
46
+ * as UTF-8; anything else is base64-encoded with `isBase64Encoded: true`
47
+ * (the REST API additionally requires matching `binaryMediaTypes` to decode
48
+ * it on the way out).
49
+ */
50
+ const responseToResult = async (response) => {
51
+ const multiValueHeaders = {};
52
+ for (const [key, value] of response.headers) {
53
+ if (key === "set-cookie") continue;
54
+ multiValueHeaders[key] = [value];
55
+ }
56
+ const setCookie = response.headers.getSetCookie();
57
+ if (setCookie.length > 0) multiValueHeaders["set-cookie"] = setCookie;
58
+ const buffer = Buffer.from(await response.arrayBuffer());
59
+ const isText = isTextContentType(response.headers.get("content-type"));
60
+ return {
61
+ statusCode: response.status,
62
+ multiValueHeaders,
63
+ body: buffer.toString(isText ? "utf8" : "base64"),
64
+ isBase64Encoded: !isText
65
+ };
66
+ };
67
+ //#endregion
68
+ //#region src/index.ts
69
+ /**
70
+ * Wrap an Elysia app as an AWS Lambda handler for API Gateway REST API
71
+ * (payload v1) proxy integration events.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { handle } from "elysia-lambda-handler";
76
+ * import { app } from "./app.js";
77
+ *
78
+ * export const handler = handle(app);
79
+ * ```
80
+ */
81
+ const handle = (app) => {
82
+ return async (event) => {
83
+ return responseToResult(await app.handle(eventToRequest(event)));
84
+ };
85
+ };
86
+ //#endregion
87
+ export { eventToRequest, handle, responseToResult };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "elysia-lambda-handler",
3
+ "version": "0.1.0",
4
+ "description": "Run Elysia apps on AWS Lambda with the Node.js runtime (API Gateway REST API events)",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/datakahashi/elysia-lambda-handler.git"
9
+ },
10
+ "homepage": "https://github.com/datakahashi/elysia-lambda-handler#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/datakahashi/elysia-lambda-handler/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.mjs",
17
+ "types": "./dist/index.d.cts",
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.mts",
22
+ "default": "./dist/index.mjs"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "keywords": [
34
+ "elysia",
35
+ "aws",
36
+ "lambda",
37
+ "api-gateway",
38
+ "handler",
39
+ "serverless"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsdown",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit"
45
+ },
46
+ "peerDependencies": {
47
+ "elysia": "^1.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/aws-lambda": "^8.10.162",
51
+ "@types/node": "^24.0.0",
52
+ "elysia": "1",
53
+ "tsdown": "^0.22.9",
54
+ "typescript": "5",
55
+ "vitest": "^4.1.10"
56
+ }
57
+ }