Decorator to return a 404 in a Nest controller
There is no built-in decorator for this, but you can create an interceptor that checks the return value and throws a NotFoundException
on undefined
:
Interceptor
@Injectable()
export class NotFoundInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle()
.pipe(tap(data => {
if (data === undefined) throw new NotFoundException();
}));
}
}
Then you can use the Interceptor
by adding it to either a single endpoint:
@Get(':id')
@UseInterceptors(NotFoundInterceptor)
findUserById(@Param() params): Promise<User> {
return this.userService.findOneById(params.id);
}
or all endpoints of your Controller
:
@Controller('user')
@UseInterceptors(NotFoundInterceptor)
export class UserController {
Dynamic Interceptor
You can also pass values to your interceptor to customize its behavior per endpoint.
Pass the parameters in the constructor:
@Injectable()
export class NotFoundInterceptor implements NestInterceptor {
constructor(private errorMessage: string) {}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
intercept(context: ExecutionContext, stream$: Observable<any>): Observable<any> {
return stream$
.pipe(tap(data => {
if (data === undefined) throw new NotFoundException(this.errorMessage);
^^^^^^^^^^^^^^^^^
}));
}
}
and then create the interceptor with new
:
@Get(':id')
@UseInterceptors(new NotFoundInterceptor('No user found for given userId'))
findUserById(@Param() params): Promise<User> {
return this.userService.findOneById(params.id);
}
The shortest way to do this would be
@Get(':id')
async findOneById(@Param() params): Promise<User> {
const user: User = await this.userService.findOneById(params.id);
if (user === undefined) {
throw new BadRequestException('Invalid user');
}
return user;
}
There is no point in decorator here because it would have the same code.
Note: BadRequestException
is imported from @nestjs/common
;
Edit
After some time with, I came with another solution, which is a decorator in the DTO:
import { registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint } from 'class-validator';
import { createQueryBuilder } from 'typeorm';
@ValidatorConstraint({ async: true })
export class IsValidIdConstraint {
validate(id: number, args: ValidationArguments) {
const tableName = args.constraints[0];
return createQueryBuilder(tableName)
.where({ id })
.getOne()
.then(record => {
return record ? true : false;
});
}
}
export function IsValidId(tableName: string, validationOptions?: ValidationOptions) {
return (object, propertyName: string) => {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
constraints: [tableName],
validator: IsValidIdConstraint,
});
};
}
Then in your DTO:
export class GetUserParams {
@IsValidId('user', { message: 'Invalid User' })
id: number;
}
Hope it helps someone.