Skip to content

Instantly share code, notes, and snippets.

@bryanjhv
Created February 17, 2026 15:45
Show Gist options
  • Select an option

  • Save bryanjhv/8303c270a04ccbdb29766ca9017e5cb2 to your computer and use it in GitHub Desktop.

Select an option

Save bryanjhv/8303c270a04ccbdb29766ca9017e5cb2 to your computer and use it in GitHub Desktop.
AWS Lambda helper for minimal size but great DX
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 || {} })
}
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