Created
February 17, 2026 15:45
-
-
Save bryanjhv/8303c270a04ccbdb29766ca9017e5cb2 to your computer and use it in GitHub Desktop.
AWS Lambda helper for minimal size but great DX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { | |
| APIGatewayProxyEventV2, | |
| APIGatewayProxyHandlerV2, | |
| APIGatewayProxyStructuredResultV2, | |
| Context, | |
| } from 'aws-lambda' | |
| import { Buffer } from 'node:buffer' | |
| export interface ApiRequest< | |
| B = Record<string, unknown>, | |
| P = Record<string, string>, | |
| Q = Record<string, string>, | |
| H = Record<string, string>, | |
| > { | |
| body: Partial<B> | |
| path: Partial<P> | |
| query: Partial<Q> | |
| headers: Partial<H> | |
| } | |
| export interface ApiResponse< | |
| D = Record<string, unknown>, | |
| E = Record<string, unknown>, | |
| > { | |
| code: number | |
| data?: D | |
| error?: { message: string } & E | |
| } | |
| // TODO: how to use type-fest Exact instead? | |
| type Exact<C, E> = C extends object | |
| ? C & { | |
| [K in keyof C]: K extends keyof E | |
| ? Exact<C[K], E[K]> | |
| : never | |
| } : C | |
| type ResolveFunction< | |
| Response extends ApiResponse<any, any>, | |
| > = <D extends NonNullable<Response['data']>>( | |
| code: number, | |
| data: Exact<D, NonNullable<Response['data']>>, | |
| ) => Response | |
| type RejectFunction< | |
| Response extends ApiResponse<any, any>, | |
| > = <E extends Omit<NonNullable<Response['error']>, 'message'>>( | |
| code: number, | |
| message: string, | |
| extra?: Exact<E, Omit<NonNullable<Response['error']>, 'message'>>, | |
| ) => Response | |
| export function handle< | |
| Request extends ApiRequest<unknown, unknown, unknown, unknown>, | |
| Response extends ApiResponse<unknown, unknown>, | |
| >( | |
| handler: ( | |
| request: Request, | |
| resolve: ResolveFunction<Response>, | |
| reject: RejectFunction<Response>, | |
| event: APIGatewayProxyEventV2, | |
| context: Context, | |
| ) => Promise<Response>, | |
| ): APIGatewayProxyHandlerV2 { | |
| return async (event, context) => { | |
| try { | |
| const text = Buffer.from( | |
| event.body || '{}', | |
| event.isBase64Encoded ? 'base64' : 'utf8', | |
| ) | |
| const request = { | |
| body: JSON.parse(text.toString('utf8')) as Request['body'], | |
| headers: event.headers as Record<string, string>, | |
| path: (event.pathParameters || {}) as Record<string, string>, | |
| query: (event.queryStringParameters || {}) as Record<string, string>, | |
| } as Request | |
| const resolve: ResolveFunction<Response> = (code, data) => ({ code, data } as Response) | |
| const reject: RejectFunction<Response> = (code, message, extra) => ({ code, error: { message, ...extra } } as Response) | |
| const response = await handler(request, resolve, reject, event, context) | |
| return result(response) | |
| } | |
| catch (err) { | |
| console.error('Handler Error:', err) | |
| return result({ | |
| code: 500, | |
| error: { message: 'Internal Server Error' }, | |
| }) | |
| } | |
| } | |
| } | |
| function json(code: number, body: unknown): APIGatewayProxyStructuredResultV2 { | |
| return { | |
| statusCode: code, | |
| isBase64Encoded: false, | |
| body: JSON.stringify(body), | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| } | |
| } | |
| function result(response: ApiResponse<unknown, unknown>): APIGatewayProxyStructuredResultV2 { | |
| if (response.error) | |
| return json(response.code, { error: { code: response.code, ...response.error } }) | |
| return json(response.code, { data: response.data || {} }) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { ApiRequest, ApiResponse } from '../lib/lambda.ts' | |
| import { handle } from '../lib/lambda.ts' | |
| type SampleRequest = ApiRequest< | |
| { bodyValue: string }, | |
| { queryId: string } | |
| > | |
| type SampleResponse = ApiResponse< | |
| { outValue: string } | |
| > | |
| export const handler = handle< | |
| SampleRequest, | |
| SampleResponse | |
| >(async (request, resolve, reject) => { | |
| const { path: { queryId }, body: { bodyValue } } = request | |
| if (!( | |
| typeof queryId === 'string' | |
| && typeof bodyValue === 'string' | |
| )) { | |
| return reject(400, 'Bad Request') | |
| } | |
| return resolve(200, { outValue: `${queryId}-${bodyValue}` }) | |
| }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment