npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, πŸ‘‹, I’m Ryan HefnerΒ  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you πŸ™

Β© 2024 – Pkg Stats / Ryan Hefner

aws-lambda-handyman

v0.5.11

Published

AWS Lambda TypeScript validation made easy πŸ„

Downloads

81

Readme

class BodyType {
  @IsEmail()
  email: string
}

class SpamBot {
  @Handler()
  static async handle(@Body() { email }: BodyType) {
    await sendSpam(email) // ->   οΈπŸ‘† I'm validated
    return ok()
  }
}

export const handler = SpamBot.handle

npm-downloads npm-version npm bundle size Test, Build and Publish coverage

Table of Contents

Installation

First off we need to install the package

npm i aws-lambda-handyman

Since we use class-validator and class-transformer under the hood we need to install them for their decorators. We also use reflect-metadata

npm i class-transformer class-validator reflect-metadata

Next we need to enable these options in our .tsconfig file

{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}

Basic Usage

AWS Lambda Handyman accpest both class-validator classes as zod parsable classes.

class-validator

import 'reflect-metadata'

class CustomBodyType {
  @IsEmail()
  email: string
}

class AccountDelete {
  @Handler()
  static async handle(@Body() { email }: CustomBodyType) {
    await deleteAccount(email)
    return ok()
  }
}

Zod

const CustomBodySchema = z.object({
  email: z.string().email()
})

class CustomBodyType {
  constructor(input: z.input<typeof CustomBodySchema>) {
    Object.assign(this, CustomBodyType.parse(input))
  }

  // Requires a static parse method
  static parse(input: unknown) {
    return new CustomBody(input as z.input<typeof CustomBodySchema>)
  }
}

class AccountDelete {
  @Handler()
  static async handle(@Body() { email }: CustomBodyType) {
    await deleteAccount(email)
    return ok()
  }
}

export const handler = AccountDelete.handle

Let's break it down.

  1. We import reflect-metadata
  2. We create a class with the shape we expect CustomBodyType
  3. We decorate the properties we want validated with any of the decorators of class-validator e.g. @IsEmail()
  4. We create a class that would hold our handler method, in this case AccountDeleteHandler and static async handle(){}
  5. We decorate handle() with the @Handler() decorator
  6. We decorate the method's parameter with @Body() and cast it to the expected shape i.e. CustomBodyType
  7. We can readily use the automatically validated method parameter, in this case the @Body() { email }: CustomBodyType

Decorators can be mixed and matched:

class KitchenSink {
  @Handler()
  static async handle(
    @Body() body: BodyType,
    @Event() evt: APIGatewayProxyEventBase<T>,
    @Paths() paths: PathsType,
    @Ctx() ctx: Context,
    @Queries() queries: QueriesType
  ) {
    return ok({ body, paths, queries, evt, ctx })
  }
}

Decorators

Method Decorators

@Handler()

This decorator needs to be applied to the handler of our http event. The handler function needs to be async or needs to return a Promise.

class AccountDelete {
  @Handler()
  static async handle() {}
}

When applied, @Handler() enables the following:

  1. Validation and injection of method parameters, decorated with @Paths(), @Body() ,@Queries() parameters
  2. Injection of method parameters, decorated with @Event() and Ctx()
  3. Out of the box error handling and custom error handling via throwing HttpError

@Handler(options?: TransformValidateOptions)

Since the aws-lambda-handyman uses class-transformer and class-validator, you can pass options to the @Handler that would be applied to the transformation and validation of the decorated method property.

import { ValidatorOptions } from 'class-validator/types/validation/ValidatorOptions'
import { ClassTransformOptions } from 'class-transformer/types/interfaces'

export type TransformValidateOptions = ValidatorOptions & ClassTransformOptions

Validation and Injection

Behind the scenes AWS Lambda Handyman uses class-validator for validation, so if any validation goes wrong we simply return a 400 with the concatenated constraints of the ValidationError[] :

class BodyType {
  @IsEmail()
  userEmail: string
  @IsInt({ message: 'My Custom error message πŸ₯Έ' })
  myInt: number
}

class SpamBot {
  @Handler()
  static async handle(@Body() { userEmail, myInt }: BodyType) {}
}

So if the preceding handler gets called with anything other than a body, with the following shape:

{
  "userEmail": "[email protected]",
  "myInt": 4321
}

The following response is sent:

HTTP/1.1 400 Bad Request
content-type: application/json; charset=utf-8

{
    "message":"userEmail must be an email. My Custom error message πŸ₯Έ."
}

If the incoming request is correct, the decorated property is injected into the method parameter and is ready for use.

Validation Caveats

By default, Path and Query parameters come in as strings, so if you try to do something like:

class PathType {
  @IsInt()
  intParam: number
}

class HandlerTest {
  @Handler()
  static async handle(@Paths() paths: PathType) {}
}

It would return an error. See Error Handling

Because aws-lambda-handyman uses class-transformer, this issue can be solved in several ways:

  1. Decorate the type with a class-transformer decorator
class PathType {
  @Type(() => Number) // πŸ‘ˆ Decorator from `class-transformer`
  @IsInt()
  intParam: number
}
  1. Enable enableImplicitConversion in @Handler(options)
class HandlerTest {
  @Handler({ enableImplicitConversion: true }) // πŸ‘ˆ
  static async handle(@Paths() paths: PathType) {}
}

Both approaches work in 99% of the time, but sometimes they don't. For example when calling:

/path?myBool=true

/path?myBool=false

/path?myBool=123

/path?myBool=1

/path?myBool=0

with

class QueryTypes {
  @IsBoolean()
  myBool: boolean
}

class HandlerTest {
  @Handler({ enableImplicitConversion: true })
  static async handle(@Queries() { myBool }: QueryTypes) {
    //            myBool is 'true'   πŸ‘†
  }
}

myBool would have the value of true. Why this happens is explained here : Class Transformer Issue 626 because of the implementation of MDN Boolean

We can fix this in the way described in Class Transformer Issue 626, or we could use @TransformBoolean like so:

class QueryTypes {
  @TransformBoolean() // πŸ‘ˆ use this πŸŽƒ
  @IsBoolean()
  myBool: boolean
}

class HandlerTest {
  @Handler()
  static async handle(@Queries() { myBool }: QueryTypes) {}
}

So when we call the handler with the previous example we get this:

/path?myBool=true πŸ‘‰ myBool = 'true'

/path?myBool=false πŸ‘‰ myBool = 'false'

/path?myBool=123 πŸ‘‰ Validation error

/path?myBool=1 πŸ‘‰ Validation error

/path?myBool=0 πŸ‘‰ Validation error

Error handling

Methods, decorated with @Handler have automatic error handling. I.e. if an error gets thrown inside the method it gets wrapped with a http response by default

class SpamBot {
  @Handler()
  static async handle() {
    throw new Error("I've fallen... and I can't get up 🐸")
  }
}

Returns:

HTTP/1.1 500 Internal Server Error
content-type: application/json; charset=utf-8

{
    "message": "I've fallen... and I can't get up 🐸"
}

We could further instrument this by throwing an HttpError() , allowing us to specify the response's message and response code:

class SpamBot {
  @Handler()
  static async handle() {
    throw new HttpError(501, 'Oopsie Doopsie 🐸')
  }
}

Which returns:

HTTP/1.1 501 Not Implemented
content-type: application/json; charset=utf-8

{
    "message": "Oopsie Doopsie 🐸"
}

You could also extend HttpError for commonly occurring error types like in DynamoError()

Function Param Decorators

@Event()

Injects the APIGatewayProxyEventBase<T> object, passed on to the function at runtime.

class AccountDelete {
  @Handler()
  static async handle(@Event() evt) {}
}

@Ctx()

Injects the Context object, passed on to the function at runtime.

class AccountDelete {
  @Handler()
  static async handle(@Ctx() context) {}
}

@Paths()

Validates the http event's path parameters and injects them into the decorated method parameter.

For example a handler, attached to the path /cars/{color} ,would look like so:

class PathType {
  @IsHexColor()
  color: string
}

class CarFetch {
  @Handler()
  static async handle(@Paths() paths: PathType) {}
}

@Body()

Validates the http event's body and injects them it into the decorated method parameter.

class BodyType {
  @IsSemVer()
  appVersion: string
}

class CarHandler {
  @Handler()
  static async handle(@Body() paths: BodyType) {}
}

@Queries()

Validates the http event's query parameters and injects them into the decorated method parameter.

For example making a http request like this /inflated?balloonId={someUUID} would be handled like this:

class QueriesType {
  @IsUUID()
  balloonId: string
}

class IsBalloonInflated {
  @Handler()
  static async handle(@Queries() queries: QueriesType) {}
}

@Headers()

Validates the http event's headers and injects them into the decorated method parameter.

For example making a http request with headers ["authorization" = "Bearer XYZ"] would be handled like this:

class HeadersType {
  @IsString()
  @IsNotEmpty()
  authoriation: string
}

class IsBalloonInflated {
  @Handler()
  static async handle(@Headers() { authoriation }: HeadersType) {}
}

Transformer Decorators

@TransformBoolean()

HttpErrors

HttpError

DynamoError

HttpResponses

response(code: number, body?: object)

ok(body?: object)

created(body?: object)

badRequest(body?: object)

unauthorized(body?: object)

notFound(body?: object)

imaTeapot(body?: object)

internalServerError(body?: object)

TODO

  • [ ] Documentation
    • [ ] add optional example
    • [ ] http responses
    • [ ] http errors
  • [ ] Linting
  • [ ] add team to collaborators