'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 |
