'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 1:[1]

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.

Solution 2:[2]

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);
}

Solution 3:[3]

Updated version of the @Kim Kern's answer for latests Nestjs versions:

As said on the Nestjs docs:

The interceptors API has also been simplified. In addition, the change was required due to this issue which was reported by the community.

Updated code:

import { Injectable, NestInterceptor, ExecutionContext, NotFoundException, CallHandler } from '@nestjs/common';
import { Observable, pipe } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class NotFoundInterceptor implements NestInterceptor {
  constructor(private errorMessage: string) { }

  intercept(context: ExecutionContext, stream$: CallHandler): Observable<any> {
    return stream$
      .handle()
      .pipe(tap(data => {
        if (data === undefined) { throw new NotFoundException(this.errorMessage); }
      }));
  }
}


Solution 4:[4]

If it's a simple case, I usually do it this lazy way without adding extra fluff:

import {NotFoundException} from '@nestjs/common'
...
@Get(':id')
async findOneById(@Param() params): Promise<User> {
    const user: User = await this.userService.findOneById(params.id)
    if (!user) throw new NotFoundException('User Not Found')
    return user
}

Solution 5:[5]

You could just use following to send your desired response in conjunction with correct status codes inside the header.

Inside your route handler in the controller class:

this.whateverService.getYourEntity(
  params.id
)
.then(result => {
  return res.status(HttpStatus.OK).json(result)
})
.catch(err => {
  return res.status(HttpStatus.NOT_FOUND).json(err)
})

For that to work you have to reject the Promise inside your service method like following:

const entity = await this.otherService
  .getEntityById(id)

if (!entity) {
  return Promise.reject({
    statusCode: 404,
    message: 'Entity not found'
  })
} 

return Promise.resolve(entity)

Here I just used another service inside the service class. You could of course just directly fetch your database or do whatever is needed to get your entity.

Solution 6:[6]

export const OnUndefined = (
  Error: new () => HttpException = NotFoundException,
) => {
  return (
    _target: unknown,
    _propKey: string,
    descriptor: PropertyDescriptor,
  ) => {
    const original = descriptor.value;
    const mayThrow = (r: unknown) => {
      if (undefined === r) throw new Error();
      return r;
    };
    descriptor.value = function (...args: unknown[]) {
      const r = Reflect.apply(original, this, args);
      if ('function' === typeof r?.then) return r.then(mayThrow);
      return mayThrow(r);
    };
  };
};

Then use like this

@Get(':id')
@OnUndefined()
async findOneById(@Param() params): Promise<User> {
    return userService.findOneById(params.id);
}

Solution 7:[7]

The OnUndefined function create e decorator that must be used as decribed above.

If the service return a undefined response (the searched id not exist) the controller return a 404 (NotFoundException) or any other excepion passed as parameter to the @OnUndefined decorator

Solution 8:[8]

The simplest solution I guess is to edit your UserService that way:

findOneById(id): Promise<User> {
  return new Promise<User>((resolve, reject) => {
    const user: User = await this.userService.findOneById(id);
    user ? 
      resolve(user) :
      reject(new NotFoundException())        
    }
}

No changes are needed on your controller.

Regards

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Maxime Lafarie
Solution 4 demisx
Solution 5
Solution 6 mister phrog
Solution 7 mister phrog
Solution 8 elkolotfi