The Nestjs module system is great, but I'm struggling to figure out how to take full advantage of it in a Serverless setting.
I like the approach of writing my domain logic in *.service.ts
files, while using *.controller.ts
files to take care of non-business related tasks such as validating an HTTP request body and converting to a DTO before invoking methods in a service.
I found the section on Serverless in the nestjs docs and determined that for my specific use-case, I need to use the "standalone application feature".
I created a sample nestjs app here to illustrate my problem.
The sample app has a simple add()
function to add two numbers. I use class-validator
for validation on the AddDto
class.
// add.dto.ts
import { IsNumber } from 'class-validator'
export class AddDto {
@IsNumber()
public a: number;
@IsNumber()
public b: number;
}
And then, via some Nestjs magic, I am able to get built-in validation using the AddDto
inside my controller by doing the following:
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Use `ValidationPipe()` for auto-validation in controllers
app.useGlobalPipes(
new ValidationPipe({ transform: true })
)
await app.listen(3000);
}
// app.controller.ts
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Post('add')
add(@Body() dto: AddDto): number {
// Request body gets auto validated and converted
// to an instance of `AddDto`, sweet!
return this.appService.add(dto.a, dto.b);
}
}
// app.service.ts
@Injectable()
export class AppService {
add(a: number, b: number): number {
return a + b
}
}
So far, so good. The problem now arises when using this in AWS with a Lambda function, namely:
app.service.ts
The docs hint on this being a problem:
Be aware that NestFactory.createApplicationContext does not wrap controller methods with enhancers (guard, interceptors, etc.). For this, you must use the NestFactory.create method.
For example, I have a lambda that receives messages from AWS EventBridge. Here's a snippet from the sample app:
// standalone-app.ts
interface IAddCommand {
a: number;
b: number;
}
export const handler = async (
event: EventBridgeEvent<'AddCommand', IAddCommand>,
context: any
) => {
const appContext = await NestFactory.createApplicationContext(AppModule);
const appService = appContext.get(AppService);
const { a, b } = event.detail;
const sum = appService.add(a, b)
// do work on `sum`, like cache the result, etc...
return sum
};
// lambda-handler.js
const { handler } = require('./dist/standalone-app')
handler({
detail: {
a: "1", // is a string, should be a number
b: "2" // is a string, should be a number
}
})
.then(console.log) // <--- prints out "12" ("1" + "2") instead of "3" (1 + 2)
I don't get "free" validation of the event's payload in event.detail
like I do with @Body() dto: AddDto
when making a HTTP POST request to /add
. Preferentially, the code would throw a validation error in the above example. Instead, I get an answer of "12"
-- a false positive.
Hopefully, this illustrates the crux of my problem. I still want to validate the payload of the event before calling appService.add(a, b)
, but I don't want to write custom validation logic that already exists on the controller in app.controller.ts
.
Ideas? Anyone else run into this before?
It occurred to me while writing this behemoth of a question that I can simply use class-validator
and class-transformer
in my Lambda handler.
import { validateOrReject } from 'class-validator'
import { plainToClass } from 'class-transformer'
import { AddDto } from 'src/dto/add.dto'
export const handler = async (event: any, context: any) => {
const appContext = await NestFactory.createApplicationContext(AppModule);
const appService = appContext.get(AppService);
const data = getPayloadFromEvent(event)
// Convert raw data to a DTO
const dto: AddDto = plainToClass(AddDto, data)
// Validate it!
await validateOrReject(dto)
const sum = appService.add(dto.a, dto.b)
// do work on `sum`...
}
It's not as "free" as using app.useGlobalPipes(new ValidationPipe())
, but only involves a few extra lines of code.