AWS Lambda TypeScript validation made easy π
npm install aws-lambda-handyman
AWS Lambda TypeScript validation made easy π ...οΈand some other things
``typescript
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

!coverage
- Installation
- Basic Usage
- Decorators
- Method Decorators
- @Handler()
- @Handler(options)
- Validation and Injection
- Validation Caveats
- Function Param Decorators
- @Event()
- @Ctx()
- @Paths()
- @Body()
- @Queries()
- @Headers()
- Transformer Decorators
- @TransformBoolean()
- Http Errors
- Http Responses
First off we need to install the package
`shell`
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
`shell`
npm i class-transformer class-validator reflect-metadata
Next we need to enable these options in our .tsconfig file
`json`
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
AWS Lambda Handyman accpest both class-validator classes as zod parsable classes.
`typescript
import 'reflect-metadata'
class CustomBodyType {
@IsEmail()
email: string
}
class AccountDelete {
@Handler()
static async handle(@Body() { email }: CustomBodyType) {
await deleteAccount(email)
return ok()
}
}
`
`typescript
const CustomBodySchema = z.object({
email: z.string().email()
})
class CustomBodyType {
constructor(input: z.input
Object.assign(this, CustomBodyType.parse(input))
}
// Requires a static parse method
static parse(input: unknown) {
return new CustomBody(input as z.input
}
}
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-metadataCustomBodyType
2. We create a class with the shape we expect @IsEmail()
3. We decorate the properties we want validated with any of
the decorators of class-validator
e.g. AccountDeleteHandler
4. We create a class that would hold our handler method, in this case static async handle(){}
and handle()
5. We decorate with the @Handler() decorator@Body()
6. We decorate the method's parameter with and cast it to the expected shape i.e. CustomBodyType@Body() { email }: CustomBodyType
7. We can readily use the automatically validated method parameter, in this case the
#### Decorators can be mixed and matched:
`typescript`
class KitchenSink {
@Handler()
static async handle(
@Body() body: BodyType,
@Event() evt: APIGatewayProxyEventBase
@Paths() paths: PathsType,
@Ctx() ctx: Context,
@Queries() queries: QueriesType
) {
return ok({ body, paths, queries, evt, ctx })
}
}
This decorator needs to be applied to the handler of our http event. The handler function needs to be async orPromise
needs
to return a .
`typescript`
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
Since the aws-lambda-handyman uses class-transformer@Handler
and class-validator, you can pass options to the that would
be applied to the transformation and validation of the decorated method property.
`typescript
import { ValidatorOptions } from 'class-validator/types/validation/ValidatorOptions'
import { ClassTransformOptions } from 'class-transformer/types/interfaces'
export type TransformValidateOptions = ValidatorOptions & ClassTransformOptions
`
Behind the scenes AWS Lambda Handyman uses class-validator for validation, so if any validation goes wrong weconstraints
simply return a 400 with the concatenated of
the [ValidationError[]](https://github.com/typestack/class-validator#validation-errors) :
`typescript
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:
`json`
{
"userEmail": "my@mail.gg",
"myInt": 4321
}
The following response is sent:
`text
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.
By default, Path and Query parameters come in as strings, so if you try to do something like:
`typescript
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
`typescriptclass-transformer
class PathType {
@Type(() => Number) // π Decorator from `
@IsInt()
intParam: number
}
2. Enable enableImplicitConversion in @Handler(options)
`typescript`
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
`typescript
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:
`typescript
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
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
`typescript`
class SpamBot {
@Handler()
static async handle() {
throw new Error("I've fallen... and I can't get up πΈ")
}
}
Returns:
`text
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:
`typescript`
class SpamBot {
@Handler()
static async handle() {
throw new HttpError(501, 'Oopsie Doopsie πΈ')
}
}
Which returns:
`text
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()
Injects the APIGatewayProxyEventBase object, passed on to the function at runtime.
`typescript`
class AccountDelete {
@Handler()
static async handle(@Event() evt) {}
}
Injects the Context object, passed on to the function at runtime.
`typescript`
class AccountDelete {
@Handler()
static async handle(@Ctx() context) {}
}
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:
`typescript
class PathType {
@IsHexColor()
color: string
}
class CarFetch {
@Handler()
static async handle(@Paths() paths: PathType) {}
}
`
Validates the http event's body and injects them it into the decorated method parameter.
`typescript
class BodyType {
@IsSemVer()
appVersion: string
}
class CarHandler {
@Handler()
static async handle(@Body() paths: BodyType) {}
}
`
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:
`typescript
class QueriesType {
@IsUUID()
balloonId: string
}
class IsBalloonInflated {
@Handler()
static async handle(@Queries() queries: QueriesType) {}
}
`
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:
`typescript
class HeadersType {
@IsString()
@IsNotEmpty()
authoriation: string
}
class IsBalloonInflated {
@Handler()
static async handle(@Headers() { authoriation }: HeadersType) {}
}
`
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)`
- [ ] Documentation
- [ ] add optional example
- [ ] http responses
- [ ] http errors
- [ ] Linting
- [ ] add team to collaborators