Features / Cheatsheet
This article assumes you have a basic understanding of NestJS and it's modular approach. The items below highlight various secondary features that address some age old issues, depending on what framework you're coming from.
A summary of features include:
Quick wins: CORS, environment variables, static directories
Payload validation, custom guards and decorators for clean validation
Events and queues for module communication using
CORS
A good example of how NestJS can make common things simple; you can enable CORS as simply as:
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3000);
}
bootstrap();
Environment Variables
To make process.env.VAR
available across your service, import the ConfigModule
as follows:
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { AppConfigService } from "./app-config/app-config.service";
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ".env",
}),
],
controllers: [],
providers: [AppConfigService],
})
export class AppModule {}
Payload Validation
NestJS provides the ValidationPipe
that will automatically validate your body using a specified DTO model. This avoids littering your controllers and services with validation logic. Furthermore, as discussed below, you can write your own decorators to ensure any specified IDs are valid.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
Guards and Decorators
To further avoid having validation logic spill into your controller and services, you can add your own guards that utilise your own services.
import {
Injectable,
CanActivate,
ExecutionContext,
NotFoundException,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Request } from "express";
import { isValidUUIDV4 } from "is-valid-uuid-v4";
import { SessionService } from "~/session/session.service";
import { WorkoutService } from "~/workout/workout.service";
@Injectable()
export class WorkoutOwnershipGuard implements CanActivate {
constructor(
private workoutService: WorkoutService,
private sessionService: SessionService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
const workoutId = request.params.workoutId;
if (!isValidUUIDV4(workoutId)) {
throw new NotFoundException("Workout ID not valid");
}
const workout = await this.workoutService.findOneById(workoutId);
if (!workout) {
throw new NotFoundException("Workout not found");
}
const user = await this.sessionService.fetchSessionUser();
if (workout.ownerId !== user.id) {
throw new NotFoundException("Workout not found");
}
return true;
}
}
Multiple Entry Points
Events
Queues
Tips
Automatically Exclude Sensitive Data
To exclude particular fields from your Entities being returned in your service responses, you can add @Exclude
as a decorator, as follows:
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { Exclude } from 'class-transformer';
@Entity()
export class YourEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
@Exclude()
sensitive: string;
...
}
To have these enforced, you need to call plainToInstance
in each endpoint:
import { Controller, Get, Param, Post, Body } from '@nestjs/common';
import { YourEntityService } from './your-entity.service';
import { plainToInstance } from 'class-transformer';
import { YourEntity } from './your-entity.entity';
@Controller('your-entity')
export class YourEntityController {
constructor(private readonly yourEntityService: YourEntityService) {}
@Get(':id')
async getEntity(@Param('id') id: number): Promise<YourEntity> {
To avoid the risk of missing one of these conversions and accidentally exposing data, we can introduce a bit of "magic" to do this automatically. We can create the following interceptor:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { plainToInstance } from "class-transformer";
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
if (Array.isArray(data)) {
return data.map((item) => plainToInstance(item.constructor, item));
} else if (data && typeof data === "object") {
return plainToInstance(data.constructor, data);
}
return data;
}),
);
}
}
This can be added to the app like so:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();
I would typically advise against this type of magic, as it can make the software harder to debug and understand. However, given it helps address a potential security risk, I think the tradeoff is justifiable in this instance.
Databased-backed Decorators
Writing your own decorators can nicely encapsulate your validations, particularly for any DTO models (as described above). An example of "the specified ID exists" can be implemented like so:
import { IsNumber, IsUUID } from "class-validator";
import { LocationEquipmentEntity } from "../location-equipment.entity";
import { EquipmentExists } from "~/equipment/decorators/equipment-exists.decorator";
export class CreateLocationEquipmentDto
implements Partial<LocationEquipmentEntity>
{
@IsUUID()
@EquipmentExists()
equipmentId: string;
}
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from "class-validator";
import { Injectable } from "@nestjs/common";
import { DataSource } from "typeorm";
import { InjectDataSource } from "@nestjs/typeorm";
import { isValidUUIDV4 } from "is-valid-uuid-v4";
import { EquipmentEntity } from "~/equipment/equipment.entity";
@ValidatorConstraint({ async: true })
@Injectable()
export class EquipmentExistsConstraint implements ValidatorConstraintInterface {
constructor(@InjectDataSource() private readonly dataSource: DataSource) {
console.log("Loaded EquipmentExistsConstraint");
}
async validate(equipmentId: any, args: ValidationArguments) {
if (!isValidUUIDV4(equipmentId)) {
return false;
}
const equipment = await this.dataSource
.getRepository(EquipmentEntity)
.findOne({ where: { id: equipmentId } });
return !!equipment;
}
defaultMessage(args: ValidationArguments) {
return "Equipment does not exist.";
}
}
export function EquipmentExists(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: EquipmentExistsConstraint,
});
};
}
Please note, in order for this to work with DTO models, you'll need to do the following:
Add the decorator to the relevant module. For example:
@Module({
imports: [
DatabaseModule,
TypeOrmModule.forFeature([LocationEntity, LocationEquipmentEntity]),
EquipmentModule,
],
controllers: [
LocationController,
LocationEquipmentController,
],
providers: [LocationService, EquipmentExistsConstraint],
exports: [LocationService],
})
export class LocationModule {}
Ensure decorators use the NestJS dependency injection by adding the following in your entry file: useContainer(app.select(AppModule), { fallbackOnErrors: true })
. A complete example might look as follows:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
useContainer(app.select(AppModule), { fallbackOnErrors: true });
app.useGlobalPipes(new ValidationPipe());
app.enableCors();
await app.listen(3000);
}
bootstrap();
Deploying to Lambda