javascriptnode.jsnestjs

Decorator to return a 404 in a Nest controller


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?


Solution

  • 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 NotFoundException('Invalid user');
        }
        return user;
    }
    

    There is no point in decorator here because it would have the same code.

    Note: NotFoundException 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.