AWS Lambda handling for HTTP
Simple wrapper for HTTP requests to manage exceptions, guarantee a response with the correct status codes and logging the exception.
Install
$ yarn add @homeservenow/serverless-aws-handler
Features
- Error logging
- Error handling - throwable exceptions that correspond to status codes
- Payload deserialisation/serialisation
- Validation + payload structuring
- Strong types
💪 - Completely customisable - use your favourite solutions
- SQS handling
Usage
import { httpHandler, NotFoundException } from '@homeservenow/serverless-aws-handler';
import { database } from './database';
export const getCatHandler = httpHandler(async ({event}): Promise<CatInterface> => {
const cat = await database().findById(event?.pathParameters?.id);
if (!cat) {
throw new NotFoundException();
}
return cat;
});
The above will result in either a 200 status code
{
"breed": "Tabby",
"lives": 7,
"name": "bob"
}
Or if NotFoundException
is thrown, a 404 status code (because of NotFoundException)
{
"message": "Not Found"
}
Post method example
You'll notice in this example, we're returning validation errors!
import { httpHandler, BadRequestException } from '@homeservenow/serverless-aws-handler';
import { database } from './database';
import { CatInterface, CatDTO } from './cat';
export const createCatHandler = httpHandler<CatInterface>({
validator: (payload: any): CatInterface => {
const validationErrors = {};
(["breed", "name", "lives"] as (keyof CatInterface)[]).forEach((property) => {
if (!payload[property]) {
validationErrors[property] = 'Cannot be blank!';
}
});
if (Object.keys(validationErrors).length >= 1) {
throw new BadRequestException('Validaiton Errors', validationErrors);
}
return new CatDTO(payload);
},
handler: async ({body}): Promise<CatInterface | never> => {
return database().create(body);
},
});
If validation errors occur then the handler will return a 400 status code plus the below body
{
"message": "Validation Errors",
"data": {
"name": "Cannot be blank!"
}
}
Request handling flow
Below is a flow chart of how we've implemented the httpHandler
to handle incoming requests. This is to give you a visual representation of the different methods.
Not Found example
import { httpHandler, NotFoundException } from '@homeservenow/serverless-aws-handler';
import {database} from './database';
export const getCatHandler = httpHandler<CatInterface>(async ({event}): Promise<CatInterface> => {
const cat = await database().find(event?.pathParameters?.id);
if (!cat) {
throw new NotFoundException();
}
return cat;
});
Results in a 404 with the payload
{
"message": "Not Found"
}
Available HTTP exceptions
Exception name | status code | default message |
---|---|---|
BadRequestException |
400 | Bad Request |
ValidationException (extends BadRequest) |
400 | Validation Errors |
UnauthorizedException |
401 | Unauthorized |
ForbiddenException |
403 | Forbidden |
NotFoundException |
404 | Not Found |
UnProcessableEntityException |
422 | Unprocessable Entity |
InternalServerError |
500 | Internal Server Error |
Available options
This is an example of all available options. All have default methods and values.
export const getCatHandler = httpHandler<InputInterface, Array<string>>({
handler: async ({event}): Promise<InputInterface | never> => { // your handler
return Promise.resolve('hello!');
},
validator: (value: any): InputInterface => { // validation method
if (!value.input) {
throw new BadRequestException();
}
return value as InputInterface;
},
defaultStatus: HttpStatusCode.NO_CONTENT, // default status code
defaultOutputHeaders: { // default return headers
['X-header']: `true`,
},
logger: (
errorHandlingOptions: ErrorHandlingOptionsType,
error: Error | HttpErrorException,
) => { // customise the logging output
console.error(error);
},
loggingHandlingOptions: HttpStatusCode.NOT_FOUND_EXCEPTION, // only log 404 errors
serialise: {
input: (event: APIGatewayEvent): any => event.body.split(''), // customised input
output: (output: Array<string>): string => output.join(''), //customised body output
},
});
Validator
The validator method is for validating the incoming structure and returned the RequestType
or required payload type for your given handler.
class CatDTO {
constructor(
readonly name: string,
readonly breed: BreedEnum,
readonly lives: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9,
) {}
}
export const myHandler = httpHandler<CatDTO>({
handler: ({body}) => {
console.log(body.name, body.breed, body.lives);
},
validator: (value: any): CatDTO => {
const cat = new CatDTO(value);
const validationErrors = {};
if (!cat.name) {
validationErrors.name = 'Name is required';
}
if (cat.lives > 9) {
validationErrors.lives = 'A cat cannot have more than 9 lives';
}
if (validationErrors.length >= 1) {
throw new BadRequestException('Validation Errors', validationErrors);
}
return cat;
},
});
Serialisation
By default, the http handler method will serialise the event body using JSON.parse. If you wish to serialise using a different method you can provide your custom method. The default for output is to convert the response from your handler to JSON.
Input
export const myHandler = httpHandler({
handler: ({body}) => {
return body; // body is returned from input method
},
serialise: {
input: (event: APIGatewayEvent): any => JSON.parse(event.body),
},
});
Output
interface UserInterface = {
password: string;
email: string;
};
export const myHandler = httpHandler<undefined, UserInterface>({
handler: (): UserInterface => {
return {
email: 'iam@email.com',
password: 'hello!Ishould-not-be-returned!',
};
},
serialise: {
output: (result: UserInterface): string => {
delete result.password;
return JSON.stringify(result);
},
},
});
Custom Error Handling
All exceptions thrown will be handled by the error handler which will convert the exception into a response object. You can override this function by providing a custom method. You could even filter out specific exceptions and return the default
import {
httpHandler,
BadRequestException,
UnprocessableEntityException,
isHttpErrorException,
httpErrorHander,
ErrorHandlingOptionsType,
HttpErrorResponseInterface,
} from '@homeservenow/serverless-aws-lambda';
export const myHandler = httpHandler<any>({
handler: ({body}) => {
if (!body) {
throw new BadRequestException();
}
if (!body.id) {
throw new UnprocessableEntityException();
}
},
errorHandler: (
errorHandlingOptions: ErrorHandlingOptionsType,
error: HttpErrorResponseInterface | Error,
): APIGatewayProxyResult => {
if (isHttpErrorException(error) && error instanceof BadRequestException) {
return {
statusCode: httpStatusCode.NO_CONTENT,
};
}
return httpErrorHandler(error);
},
});
Default status code and headers
The below example will always output a status code of 204.
export const myHandler = httpHandler({
handler: () => {
},
defaultStatusCode: HttpStatusCode.NO_CONTENT,
});
default Headers
The below example will always output a header X-header
export const myHandler = httpHandler({
handler: () => {
},
defaultHeaders: {
['X-header']: 'Hello!',
},
});
Exception logging options
The httpHandler has its own default logging method which you can customise. This method will make info logs, warn logs and error logs using console.
There are several different logging methods
- By http status
- Range by http status
- Blacklist | whitelist
- boolean
Logging is only called when an exception occurs. All of the below examples are generally for status codes of 400 and above. However it is possible to override.
Singular http status
Will only log INTERNAL_SERVER_ERROR
(500) status codes.
export const myHandler = httpHandler({
handler: myHandlerMethod,
loggingOptions: HttpStatusCode.INTERNAL_SERVER_ERROR,
});
Range by http status
A range will only log errors within the target range including the given values. In this example all exceptions with a status code between 400
and 403
will be logged. 500
Status codes will not be logged in this example.
export const myHandler = httpHandler({
handler: myHandlerMethod,
loggingOptions: [HttpStatusCode.BAD_REQUEST, HttpStatusCode.FORBIDDEN],
});
Blacklist
If you was to use the blacklist without the whitelist, any status code not in the array will be logged. Anything other than 400 and 401 will be logged here.
export const myHandler = httpHandler({
handler: myHandlerMethod,
loggingOptions: {
blacklist: [
HttpStatusCode.BAD_REQUEST,
HttpStatusCode.UNAUTHORIZED,
],
},
});
Whitelist
In this example, only 400 status code exceptions will be logged.
export const myHandler = httpHandler({
handler: myHandlerMethod,
loggingOptions: {
whitelist: [
HttpStatusCode.BAD_REQUEST,
],
},
});
Blacklist | Whitelist
In this example, any status code within a whitelist will be logged. Anything in the blacklist will not be logged. However. If the given status code is in both provided arrays, then the blacklist overrides the whitelist and the error will not be logged.
In the given example, only forbidden status codes will be logged. Any other status code will not.
export const myHandler = httpHandler({
handler: myHandlerMethod,
loggingOptions: {
blacklist: [
HttpStatusCode.BAD_REQUEST,
HttpStatusCode.UNAUTHORIZED,
],
whitelist: [
HttpStatusCode.BAD_REQUEST,
HttpStatusCode.FORBIDDEN,
],
},
});
SQS handler
A basic wrapper for exceptions, logging and deleting messages
Basic usage
import {sqsHandler, SQSHandleActions} from '@homeservenow/serverless-aws-handler';
import {PayloadInterface} from './payload';
import {sqs} from './sqs';
export const handler = sqsHandler<PayloadInterface>(sqs)(async (payload: PayloadInterface): Promise<SQSHandleActions> => {
console.log('payload', payload);
return Promise.resolve(SQSHandleActions.DELETE);
});
Actions
All actions are defined in the enum SQSHandleActions
which has the below actions
Action name | action |
---|---|
DELETE |
Deletes the record from the queue (accepts) |
DEAD_LETTER |
Let's AWS SQS handle the message with a dead letter (does nothing) |
Filtering
For any reason you want to remove duplicated records or filter out certain properties or values, use the filterUniqueRecords
property.
export const handler = sqsHandler<PayloadInterface>(sqs)({
handler: async (payload: PayloadInterface): Promise<SQSHandleActions> => {
console.log('payload', payload);
return Promise.resolve(SQSHandleActions.DELETE);
},
filterUniqueRecords: (records: SQSRecord[]): SQSRecord[] => records.filter(record => record.MessageId !== 'remove'),
});
Exception handling
The default exception handling function will delete your message from the queue. If you wish to handle this differently, you can use the exceptionHandler
function to return your desired action.
import {sqsHandler, SQSHandleActions, RecordResults} from '@homeservenow/serverless-aws-handler';
export const handler = sqsHandler<PayloadInterface>(sqs)({
handler: async (payload: PayloadInterface): Promise<SQSHandleActions> => {
console.log('payload', payload);
throw new Error('Exception handler call');
return Promise.resolve(SQSHandleActions.DELETE);
},
exceptionHandler: (record: SQSRecord): Promise<RecordResults> => Promise.resolve({
record,
result: SQSHandleActions.DEAD_LETTER, // will do nothing
}),
});
Error logging
Whenever an exception is thrown, a logger function is called. The default will log the error as erronous to the console. However if you wish you add your own functionality, use the logger
property to define your own
import {sqsHandler, SQSHandleActions, RecordResults} from '@homeservenow/serverless-aws-handler';
export const handler = sqsHandler<PayloadInterface>(sqs)({
handler: async (payload: PayloadInterface): Promise<SQSHandleActions> => {
console.log('payload', payload);
throw new Error('Exception handler call');
return Promise.resolve(SQSHandleActions.DELETE);
},
logger: (error: any): void => {
// Send error to sentry or whatever
console.error({
severity: "error",
error,
});
},
});
Serialising
import {sqsHandler, SQSHandleActions, RecordResults} from '@homeservenow/serverless-aws-handler';
import { CustomType } from './customType';
export const handler = sqsHandler<CustomType>(sqs)({
handler: async (payload: CustomType): Promise<SQSHandleActions> => {
console.log('payload', payload);
return Promise.resolve(SQSHandleActions.DELETE);
},
serialise: (record: SQSRecord): CustomType => {
const json = JSON.parse(record.body);
return new CustomType(json);
},
});