I'm working on a backend using NestJS, (which is amazing btw). I have a 'standard get a single instance of an entity situation' similar to this example below.
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
..
..
..
@Get(':id')
async findOneById(@Param() params): Promise<User> {
return userService.findOneById(params.id);
}
This is incredibly simple and works - however, if the user does not exist, the service returns undefined and the controller returns a 200 status code and an empty response.
In order to make the controller return a 404, I came up with the following:
@Get(':id')
async findOneById(@Res() res, @Param() params): Promise<User> {
const user: User = await this.userService.findOneById(params.id);
if (user === undefined) {
res.status(HttpStatus.NOT_FOUND).send();
}
else {
res.status(HttpStatus.OK).json(user).send();
}
}
..
..
This works, but is a lot more code-y (yes it can be refactored).
This could really use a decorator to handle this situation:
@Get(':id')
@OnUndefined(404)
async findOneById(@Param() params): Promise<User> {
return userService.findOneById(params.id);
}
Anyone aware of a decorator that does this, or a better solution than the one above?
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.
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, stream$: Observable<any>): Observable<any> {
// stream$ is an Observable of the controller's result value
return stream$
.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);
}