Skip to content

Repository files navigation

AWS Lambda wrapper library

This documentation is for v4 of the library - go here for v1, v2, and v3 documentation.

  1. Overview
  2. Installation and setup
  3. Supported events
  4. Example projects

Overview

TL;DR

This library provides custom Lambda function wrappers which expose standard, abstracted functionality so that developers can focus on writing business logic instead of parsing event payloads and crafting response objects.

Rationale and motivation

AWS Lambda supports a wide variety of event triggers, each with unique payloads and expected response objects. The Lambda method signature, however, only provides a raw event object and has no included mechanisms for simplifying payload parsing or response object creation. For example API Gateway events include only the raw request body, leaving it up to developers to implement parsing themselves. Similarly the developer is responsible for creating a response object which includes the correct HTTP status code and headers. This library exposes helpful abstractions like parsed HTTP bodies based on content-type headers, and success functions which create response objects with the correct status codes and headers for returning to API Gateway.

Feedback is appreciated! If you have an idea for how this library can be improved (or just a complaint/criticism) then please open an issue.

Installation and setup

Install and save the package:

npm i -S @manwaring/lambda-wrapper

yarn add @manwaring/lambda-wrapper

Optional configuration

If you want the wrapper to log request and response messages (helpful for debugging) set an environemnt variable for LAMBDA_WRAPPER_LOG=true.

If you want each invocation to be tagged with the AWS region, stage/environment, and Git revision simply set environment variables for each and the library will pick them up, for example REGION=us-east-1, STAGE=prod, REVISION=f4ba682. See git-rev-sync and serverless-plugin-git-variables for libraries that can help you set git revision automatically.

Supported events

Each event listed here has a wrapper which provides a deconstructable method signature exposing parsed/unmarshalled request parameters and helper response methods.

  1. API Gateway
  2. API Gateway HTTP API
  3. CloudFormation Custom Resource
  4. DynamoDB Stream
  5. Lambda Authorizer
  6. SNS
  7. Generic event (a basic wrapper with support for success and failure responses)

API Gateway

Sample TypeScript implementation

import{api}from'@manwaring/lambda-wrapper';import{CustomInterface}from'./custom-interface';import{doSomething}from'./you-code';exportconsthandler=api<CustomInterface>(async({ body, path, success, invalid, error })=>{try{const{ pathParam1, pathParam2 }=path;if(!pathParam1){returninvalid();}constresults=awaitdoSomething(body,pathParam1,pathParam2);returnsuccess({body: results});}catch(err){returnerror({ err });}});

By passing in CustomInterface as a generic type the method signature will cast the body object as an instance of CustomInterface, making TypeScript development easier. Note that the type is not required and the body property defaults to type any.

Sample implementation without generic
import{api}from'@manwaring/lambda-wrapper';import{doSomething}from'./you-code';exportconsthandler=api(async({ body, path, success, invalid, error })=>{try{const{ pathParam1, pathParam2 }=path;if(!pathParam1){returninvalid();}constresults=awaitdoSomething(body,pathParam1,pathParam2);returnsuccess({body: results});}catch(err){returnerror({ err });}});

Properties and methods available on wrapper signature

Deconstructable wrapper signature

Note that all properties are undefined if not present on the original request.

exportinterfaceApiSignature<T=any>{event: APIGatewayEvent;// original event provided by AWSbody: T;// body payload parsed according to content-type headers (or raw if no content-type headers found) and cast as T if provided (defaults to `any`)websocket: WebsocketRequest;// websocket connection payloadpath: {[name: string]: string};// path params as key-value pairsquery: {[name: string]: string};// query params as key-value pairsheaders: {[name: string]: string};// headers as key-value pairstestRequest: boolean;// indicates if this is a test request - looks for a header matching process.env.TEST_REQUEST_HEADER (dynamic from application) or 'Test-Request' (default)auth: any;// auth context from custom authorizersuccess(params?: ResponseParameters): ApiResponse;invalid(params?: ResponseParameters): ApiResponse;notFound(params?: ResponseParameters): ApiResponse;notAuthorized(params?: ResponseParameters): ApiResponse;redirect(params: RedirectParameters): ApiResponse;error(params?: ErrorParameters): ApiResponse;custom(params: CustomParameters): ApiResponse;}
ApiResponse
interfaceApiResponse{statusCode: number;headers: {[name: string]: any};body?: string;}
ResponseParameters
interfaceResponseParameters{body?: any;// response bodyjson?: boolean;// indicates if body should be JSON-stringified and content-type header set to application/json, defaults to truecors?: boolean;// indicates if CORS headers should be added, defaults to truestatusCode?: number;// status code to return, defaults by callback (success: 200, invalid: 400, notFound: 404, notAuthorized: 401, redirect: 302, error: 500)headers?: {[key: string]: any};// custom headers to include}
RedirectParameters
interfaceRedirectParameters{url: string;// url to redirect tocors?: boolean;// indicates if CORS headers should be added, defaults to truestatusCode?: number;// status code to return, defaults to 302headers?: {[key: string]: any};// custom headers to include}
ErrorParameters
interfaceErrorParameters{body?: any;// response bodyjson?: boolean;// indicates if body should be JSON-stringified and content-type header set to application/json, defaults to truecors?: boolean;// indicates if CORS headers should be added, defaults to truestatusCode?: number;// status code to return, defaults to 500headers?: {[key: string]: any};// custom headers to includeerr?: Error;// optional Error object for automatic logging}
CustomParameters
interfaceCustomParameters{body?: any;// response bodyjson?: boolean;// indicates if body should be JSON-stringified and content-type header set to application/json, defaults to truecors?: boolean;// indicates if CORS headers should be added, defaults to truestatusCode: number;// status code to returnheaders?: {[key: string]: any};// custom headers to includeerr?: Error;// optional Error object for automatic logging}
WebsocketRequest
exportinterfaceWebsocketRequest{accountId: string;apiId: string;connectedAt?: number;connectionId?: string;domainName?: string;domainPrefix?: string;eventType?: string;extendedRequestId?: string;protocol: string;httpMethod: string;identity: APIGatewayEventIdentity;messageDirection?: string;messageId?: string|null;path: string;stage: string;requestId: string;requestTime?: string;requestTimeEpoch: number;resourceId: string;resourcePath: string;routeKey?: string;}

Response functions

Success

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode?: number,headers?: {[key: string]: any}}

Default parameters

{json: true,cors: true,statusCode: 200}

Invocation with defaults

constresponse={hello: 'world'};returnsuccess({body: response});// returns{body: "{\"hello\":\"world\"}",statusCode: 200,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,'Content-Type': 'application/json'}}

Invocation overriding defaults

constresponse='<svg xmlns="http://www.w3.org/2000/svg"></svg>';constheaders={'Content-Type': 'image/svg+xml'};returnsuccess({body: response,json: false,cors: false, headers });// returns{body: "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>",statusCode: 200,headers: {'Content-Type': 'image/svg+xml'}}
Invalid

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode?: number,headers?: {[key: string]: any}}

Default parameters

{json: true,cors: true,statusCode: 400}

Invocation with defaults

returninvalid();// returns{statusCode: 400,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

Invocation overriding defaults

constresponse={invalid: 'properties'};returninvalid({body: response,cors: false});// returns{body: "{\"invalid\":\"properties\"}",statusCode: 400,headers: {'Content-Type': 'application/json'}}
Not found

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode?: number,headers?: {[key: string]: any}}

Default parameters

{json: true,cors: true,statusCode: 404}

Invocation with defaults

returnnotFound();// returns{statusCode: 404,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

Invocation overriding defaults

constresponse='Not found';returnnotFound({body: response,cors: false});// returns{body: "Not found",statusCode: 404,}
Not authorized

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode?: number,headers?: {[key: string]: any}}

Default parameters

{json: true,cors: true,statusCode: 401}

Invocation with defaults

returnnotAuthorized();// returns{statusCode: 401,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

Invocation overriding defaults

constresponse='Not authorized';returnnotAuthorized({body: response,cors: false});// returns{body: "Not Authorized",statusCode: 401,}
Redirect

Available parameters

{url: string,cors?: boolean,statusCode?: number,headers?: {[key: string]: any}}

Default parameters

{cors: true,statusCode: 302}

Invocation with defaults

consturl='https://github.com/manwaring/lambda-wrapper';returnredirect({ url });// returns{statusCode: 302,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,'Location': 'https://github.com/manwaring/lambda-wrapper'}}

Invocation overriding defaults

consturl='https://github.com/manwaring/lambda-wrapper';returnredirect({ url,statusCode: 308,cors: false});// returns{statusCode: 308,headers: {'Location': 'https://github.com/manwaring/lambda-wrapper'}}
Error

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode?: number,headers?: {[key: string]: any},err?: Error}

Default parameters

{json: true,cors: true,statusCode: 500}

Invocation with defaults

returnerror();// returns{statusCode: 500,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

Invocation overriding defaults

catch(err){constbody={error: 'Unexpected error'};returnerror({ body, err });}// logsconsole.debug(err);// returns{body: "{\"error\": \"Unexpected error\"}",statusCode: 500,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}
Custom

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode: number,headers?: {[key: string]: any},err?: Error}

Default parameters

{json: true,cors: true,}

Invocation with defaults

returncustom({statusCode: 418});// returns{statusCode: 418,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

Invocation overriding defaults

constbody={message: 'Custom response'};returncustom({ body,statusCode: 418});// returns{body: "{\"message\": \"Custom response\"}",statusCode: 418,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

API Gateway HTTP API

Other than the raw payload from AWS the HTTP API method signature and response functions match the API Gateway signature and functions. Hooray for wrappers! Note that you still need to provide the correct wrapper function so that the library can parse the AWS event correctly.

Sample TypeScript implementation

import{httpApi}from'@manwaring/lambda-wrapper';import{CustomInterface}from'./custom-interface';import{doSomething}from'./you-code';exportconsthandler=httpApi<CustomInterface>(async({ body, path, success, invalid, error })=>{try{const{ pathParam1, pathParam2 }=path;if(!pathParam1){returninvalid();}constresults=awaitdoSomething(body,pathParam1,pathParam2);returnsuccess({body: results});}catch(err){returnerror({ err });}});

By passing in CustomInterface as a generic type the method signature will cast the body object as an instance of CustomInterface, making TypeScript development easier. Note that the type is not required and the body property defaults to type any.

Sample implementation without generic
import{httpApi}from'@manwaring/lambda-wrapper';import{doSomething}from'./you-code';exportconsthandler=httpApi(async({ body, path, success, invalid, error })=>{try{const{ pathParam1, pathParam2 }=path;if(!pathParam1){returninvalid();}constresults=awaitdoSomething(body,pathParam1,pathParam2);returnsuccess({body: results});}catch(err){returnerror({ err });}});

Properties and methods available on wrapper signature

Deconstructable wrapper signature

Note that all properties are undefined if not present on the original request.

exportinterfaceHttpApiSignature<T=any>{event: HttpApiEvent;// original event provided by AWSbody: T;// body payload parsed according to content-type headers (or raw if no content-type headers found) and cast as T if provided (defaults to `any`)rawPath: string;// the endpoint path used to invoke this Lambdapath: {[name: string]: string};// path params as key-value pairsquery: {[name: string]: string};// query params as key-value pairsrawQueryString: string// the raw query string from the requestheaders: {[name: string]: string};// headers as key-value pairstestRequest: boolean;// indicates if this is a test request - looks for a header matching process.env.TEST_REQUEST_HEADER (dynamic from application) or 'Test-Request' (default)auth: any;// auth context from JWT authorizersuccess(params?: ResponseParameters): ApiResponse;invalid(params?: ResponseParameters): ApiResponse;notFound(params?: ResponseParameters): ApiResponse;notAuthorized(params?: ResponseParameters): ApiResponse;redirect(params: RedirectParameters): ApiResponse;error(params?: ErrorParameters): ApiResponse;custom(params: CustomParameters): ApiResponse;}
ApiResponse
interfaceApiResponse{statusCode: number;headers: {[name: string]: any};body?: string;}
ResponseParameters
interfaceResponseParameters{body?: any;// response bodyjson?: boolean;// indicates if body should be JSON-stringified and content-type header set to application/json, defaults to truecors?: boolean;// indicates if CORS headers should be added, defaults to truestatusCode?: number;// status code to return, defaults by callback (success: 200, invalid: 400, notFound: 404, notAuthorized: 401, redirect: 302, error: 500)headers?: {[key: string]: any};// custom headers to include}
RedirectParameters
interfaceRedirectParameters{url: string;// url to redirect tocors?: boolean;// indicates if CORS headers should be added, defaults to truestatusCode?: number;// status code to return, defaults to 302headers?: {[key: string]: any};// custom headers to include}
ErrorParameters
interfaceErrorParameters{body?: any;// response bodyjson?: boolean;// indicates if body should be JSON-stringified and content-type header set to application/json, defaults to truecors?: boolean;// indicates if CORS headers should be added, defaults to truestatusCode?: number;// status code to return, defaults to 500headers?: {[key: string]: any};// custom headers to includeerr?: Error;// optional Error object for automatic logging}
CustomParameters
interfaceCustomParameters{body?: any;// response bodyjson?: boolean;// indicates if body should be JSON-stringified and content-type header set to application/json, defaults to truecors?: boolean;// indicates if CORS headers should be added, defaults to truestatusCode: number;// status code to returnheaders?: {[key: string]: any};// custom headers to includeerr?: Error;// optional Error object for automatic logging}
HttpApiEvent

AWS documentation of raw event

exportinterfaceHttpApiEvent{version: string;routeKey: string;rawPath: string;rawQueryString: string;cookies: string[];headers: {[key: string]: string};queryStringParameters: {[key: string]: string};requestContext: {accountId: string;apiId: string;authorizer: {jwt: {claims: {[key: string]: string};scopes: string[];};};domainName: string;domainPrefix: string;http: {method: string;path: string;protocol: string;sourceIp: string;userAgent: string;};requestId: string;routeKey: string;stage: string;time: string;timeEpoch: number;};body: string;pathParameters: {[key: string]: string};isBase64Encoded: boolean;stageVariables: {[key: string]: string};}

Response functions

Success

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode?: number,headers?: {[key: string]: any}}

Default parameters

{json: true,cors: true,statusCode: 200}

Invocation with defaults

constresponse={hello: 'world'};returnsuccess({body: response});// returns{body: "{\"hello\":\"world\"}",statusCode: 200,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,'Content-Type': 'application/json'}}

Invocation overriding defaults

constresponse='<svg xmlns="http://www.w3.org/2000/svg"></svg>';constheaders={'Content-Type': 'image/svg+xml'};returnsuccess({body: response,json: false,cors: false, headers });// returns{body: "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>",statusCode: 200,headers: {'Content-Type': 'image/svg+xml'}}
Invalid

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode?: number,headers?: {[key: string]: any}}

Default parameters

{json: true,cors: true,statusCode: 400}

Invocation with defaults

returninvalid();// returns{statusCode: 400,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

Invocation overriding defaults

constresponse={invalid: 'properties'};returninvalid({body: response,cors: false});// returns{body: "{\"invalid\":\"properties\"}",statusCode: 400,headers: {'Content-Type': 'application/json'}}
Not found

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode?: number,headers?: {[key: string]: any}}

Default parameters

{json: true,cors: true,statusCode: 404}

Invocation with defaults

returnnotFound();// returns{statusCode: 404,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

Invocation overriding defaults

constresponse='Not found';returnnotFound({body: response,cors: false});// returns{body: "Not found",statusCode: 404,}
Not authorized

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode?: number,headers?: {[key: string]: any}}

Default parameters

{json: true,cors: true,statusCode: 401}

Invocation with defaults

returnnotAuthorized();// returns{statusCode: 401,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

Invocation overriding defaults

constresponse='Not authorized';returnnotAuthorized({body: response,cors: false});// returns{body: "Not Authorized",statusCode: 401,}
Redirect

Available parameters

{url: string,cors?: boolean,statusCode?: number,headers?: {[key: string]: any}}

Default parameters

{cors: true,statusCode: 302}

Invocation with defaults

consturl='https://github.com/manwaring/lambda-wrapper';returnredirect({ url });// returns{statusCode: 302,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,'Location': 'https://github.com/manwaring/lambda-wrapper'}}

Invocation overriding defaults

consturl='https://github.com/manwaring/lambda-wrapper';returnredirect({ url,statusCode: 308,cors: false});// returns{statusCode: 308,headers: {'Location': 'https://github.com/manwaring/lambda-wrapper'}}
Error

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode?: number,headers?: {[key: string]: any},err?: Error;}

Default parameters

{json: true,cors: true,statusCode: 500}

Invocation with defaults

returnerror();// returns{statusCode: 500,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

Invocation overriding defaults

catch(err){constbody={error: 'Unexpected error'};returnerror({ body, err });}// logsconsole.debug(err);// returns{body: "{\"error\": \"Unexpected error\"}",statusCode: 500,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}
Custom

Available parameters

{body?: any,json?: boolean,cors?: boolean,statusCode: number,headers?: {[key: string]: any},err?: Error}

Default parameters

{json: true,cors: true,}

Invocation with defaults

returncustom({statusCode: 418});// returns{statusCode: 418,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

Invocation overriding defaults

constbody={message: 'Custom response'};returncustom({ body,statusCode: 418});// returns{body: "{\"message\": \"Custom response\"}",statusCode: 418,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': true,}}

CloudFormation Custom Resource

Sample TypeScript implementation

import{cloudFormation}from'@manwaring/lambda-wrapper';exportconsthandler=cloudFormation(({ event, success, failure })=>{try{const{ BucketName }=event.ResourceProperties;returnsuccess();}catch(err){returnfailure(err);}});

*Note that currently the method wrapped by cloudFormation cannot be async - for reasons that aren't entirely clear to me when the method is async the requests to update CloudFormation with the correct action status fail, leaving a stack in the 'pending' state

Properties and methods available on wrapper signature

interfaceCloudFormationSignature{event: CloudFormationCustomResourceEvent;// original eventsuccess(payload?: any): void;// sends CloudFormation success eventfailure(message?: any): void;// sends CloudFormation failure event}

DynamoDB Stream

Sample TypeScript implementation

import{dynamodbStream}from'@manwaring/lambda-wrapper';import{CustomInterface}from'./custom-interface';// By passing in CustomInterface as a generic the async method signature will correctly identify newVersions as an array of CustomInterface, making TypeScript development easier (note that the generic is not required in JavaScript projects)exportconsthandler=dynamodbStream<CustomInterface>(async({ newVersions, success, error })=>{try{newVersions.forEach((version)=>console.log(version));returnsuccess(newVersions);}catch(err){returnerror(err);}});interfaceCustomInterface{id: number;value: string;}

Properties and methods available on wrapper signature

interfaceDynamoDBStreamSignature<T>{event: DynamoDBStreamEvent;// original eventnewVersions: T[];// array of all unmarshalled javascript objects of new imagesoldVersions: T[];// array of all unmarshalled javascript objects of old imagesversions: Version<T>[];// array of full version object (new image, old image, etc - see Version interface)success(message?: any): any;// logs and returns the messageerror(error?: any): void;// logs the error and throws it}interfaceVersion<T>{newVersion: T;// unmarshalled javascript object of new image (if exists) or nulloldVersion: T;// unmarshalled javascript object of old image (if exists) or nullkeys: any;// unmarshalled javascript object of keys (includes key values)tableName: string;// name of the table the object came fromtableArn: string;// arn of the table the object came fromeventName: 'INSERT'|'MODIFY'|'REMOVE';// name of the event (INSERT || MODIFY || REMOVE)}

Lambda Authorizer

Sample TypeScript implementation

import{authorizer}from'@manwaring/lambda-wrapper';constverifier=newVerifier();// setup and configure JWT validation libraryexportconsthandler=authorizer(async({ token, valid, invalid })=>{try{if(!token){returninvalid('Missing token');}constjwt=awaitverifier.verifyAccessToken(token);returnvalid(jwt);}catch(err){returninvalid(err);}});

Properties and methods available on wrapper signature

interfaceAuthorizerSignature{event: CustomAuthorizerEvent;// original eventtoken: string;// authorizer token from original eventvalid(jwt: any): Policy;// returns AWS policy to authenticate request, and adds auth context if availableinvalid(message?: any): void;// records invalid information and throws 401 unauthorizederror(error?: any): void;// records error information and throws 401 unauthorized}interfacePolicy{principalId: string;policyDocument: {Version: string;Statement: {Action: string;Effect: string;Resource: string;}[];};}

SNS

Sample TypeScript implementation

import{sns}from'@manwaring/lambda-wrapper';import{CustomInterface}from'./custom-interface';// By passing in CustomInterface as a generic the async method signature will correctly identify newVersions as an array of CustomInterface, making TypeScript development easier (note that the generic is not required in JavaScript projects)exportconsthandler=sns<CustomInterface>(async({ message, success, error })=>{try{console.log(message);returnsuccess();}catch(err){returnerror(err);}});

Properties and methods available on wrapper signature

interfaceSnsSignature{event: SNSEvent;// original eventmessage: any;// JSON-parsed message from eventsuccess(message?: any): any;// logs and returns the messageerror(error?: any): void;// logs the error and throws}

Generic event

Sample TypeScript implementation

import{wrapper}from'@manwaring/lambda-wrapper';import{CustomInterface}from'./custom-interface';// By passing in CustomInterface as a generic the async method signature will correctly identify newVersions as an array of CustomInterface, making TypeScript development easier (note that the generic is not required in JavaScript projects)exportconsthandler=wrapper<CustomInterface>(async({ event, success, error })=>{try{const{ value1, value2 }=event;constresults=awaitdoSomething(value1,value2);returnsuccess(results);}catch(err){returnerror(err);}});

Properties and methods available on wrapper signature

interfaceWrapperSignature<T>{event: T;// original eventsuccess(message?: any): any;// logs and returns the messageerror(error?: any): void;// logs the error and throws}

Example projects

There is one working example of how this package can be used in a simple 'hello world' serverless application:

  1. Using the Serverless Framework and TypeScript

About

A JavaScript library which provides wrappers for AWS Lambda event triggers and responses - e.g. automatically parsing request bodies and helper functions for returning well-structured HTTP responses

Topics

Resources

Stars

16 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages