400. More Building Blocks
Intro
- In addition to Modules, Controllers and Providers, nest has other building blocks it uses in its architecture to make nest extendable and maintainable, there are
- Exception filters
- specialized class that intercepts and handles uncaught exceptions thrown within the application.
- It enables centralized error handling, customizable error responses.
- Guards
- control access to route handlers based on custom criteria, like user roles or permissions.
- Interceptors
- Enable aspect-oriented programming with functionality like response transformation, error handling, and caching
- pipes
- Transform input data or validate request payloads before reaching route handlers.
- And middleware
- Custom functions that execute before reaching the main route handler. Middleware can perform tasks like logging, authentication, and data manipulation.
- Exception filters
How and Why to Use Them ?
Exception Filters
- Exception filters | NestJS - A progressive Node.js framework
- can be used for?
- Centralized error handling: Exception filters allow you to handle all errors in a centralized location, rather than scattering error handling logic throughout your application. This leads to cleaner, more maintainable code.
- Custom error responses: By using exception filters, you can customize the error response sent to the client.
- Logging and monitoring: Exception filters provide a single point where you can log and monitor errors that occur in your application.
- Separation of concerns: Exception filters help separate error handling logic from the main application logic.
nest g filter path/filter-name
- Throwing standard exception
//in nest js you can throw a standard exception from your endpoints like this.
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
response = {
statusCode: 403,
message: "Forbidden"
}
Creating Exception Filters
- Exception filters | NestJS - A progressive Node.js framework
- Let's create an exception filter that is responsible for catching exceptions which are an instance of the HttpException class, and implementing custom response logic for them.
@Catch(HttpException) //<--- catching HttpException only
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(), //new field
path: request.url, //new field
});
}
}
- Use it at method level, applies for the method's response only
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
//new updated response
response = {
statusCode: 403,
timestamp: "'2023-07-11T09:22:03.242Z'",
path: "request path"
}
- use it at controller level, applies for all methods' response inside the controller file
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
- use it at module level, applies for all exception responses found in that module's controllers
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
- use it inside main.ts, applies across the app entirely
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
Guards
- Guards | NestJS - A progressive Node.js framework
- can be used for?
- Authentication, authorization or to limit execution based on certain information.
- token based authentication…
- We can create custom guards, i.e. @isMobile to only let mobile devices access a method or a controller…
- This pattern is called custom decorator guards.
nest g guard path/guard-name
If you are looking for a real-world example on how to implement an authentication mechanism in your application, check Authentication | NestJS - A progressive Node.js framework. Likewise, for more sophisticated authorization example, check Authorization | NestJS - A progressive Node.js framework
Creating Custom API Guard
- create a new decorator with
setMetaData
- use the decorator inside a controller's method or class
- use the metadata information inside guards logic
- introduce your new guard to nest at specific level
import { SetMetadata } from "@nestjs/common";
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@Controller('coffes')
export class CoffesController {
constructor(private readonly coffesService: CoffesService) {}
@Public() //custom decorator
@Get()
findAll(@Query() paginationQuery: PaginationDTO) {
return this.coffesService.findAll(paginationQuery);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.coffesService.findOne(id);
}
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.get<boolean>(
IS_PUBLIC_KEY,
context.getHandler(),
);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest();
const authHeader = request.header('Authorization');
return authHeader === '123';
}
}
//registering our new guard inside main.ts
app.useGlobalGuards(new ApiKeyGuard());
Pipes
- Pipes | NestJS - A progressive Node.js framework
- Nest already comes with 9 pipes available out-of-the-box:
nest g pipe path/pipe-name
- Can be used for?
- Transformation: transform input data to the desired form
- (e.g., from string to integer)
- Validation: evaluate input data and if valid, simply pass it through unchanged; otherwise, throw an exception
- Transformation: transform input data to the desired form
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
//if id is invalid the following response will be returned
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
Interceptors
- Interceptors | NestJS - A progressive Node.js framework
nest g interceptor path/interceptor-name
- Can be used for?
- Add extra logic before or after method execution
- add logging, persist request time diff…
- Transform a result returned from a method
- i.e. wrap response in
{data: response}
- i.e. wrap response in
- transform exception thrown from a method
- cache responses
- terminate a request after x-seconds if no response
- Add extra logic before or after method execution
Where Can We Use These Blocks?
- You can use filters, guards, interceptors and pipes at different levels in nest, levels being controller, method, module and global.
- You need to use different syntax at each level
- global(inside main.ts)
app.useGlobalFilter(new FilterName())
app.useGlobalGuards(new GuardName())
app.useGlobalInterceptors(new IntercepterName())
app.useGlobalPipes(new PipeName())
- module
- when you use a building blocks in a module level, it will be available to all components the module define.
- ex:
{provide: APP_FILTER, useClass: FilterName }
- ex:
- when you use a building blocks in a module level, it will be available to all components the module define.
- Controller
- no need to use new keyword
@UsePipes(FilterName)
@UseGuards(GuardName)
@UseIntercepters(IntercepterName)
@UseFilters(FilterName)
- method
- same as controller
- parameter
- only pipes work at parameter level
- i.e.
@Body(ValidationPipe)
- global(inside main.ts)
Middleware
- Middleware | NestJS - A progressive Node.js framework
- Nest middleware are, by default, equivalent to express middleware.
- Can be used for to?
- execute any code before/after request.
- Make changes to the request and the response objects.
- End the request-response cycle.
- Call the next middleware function in the stack.
Create New Middleware
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
Using a Middleware
- Using middleware | NestJS - A progressive Node.js framework
- There is no place for middleware in the @Module() decorator. Instead, we set them up using the configure() method of the module class.
- Modules that include middleware have to implement the NestModule interface. Let's set up the LoggerMiddleware at the AppModule level.
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}
Global Middleware
- If we want to bind middleware to every registered route at once, we can use the use() method that is supplied by the INestApplication instance:
const app = await NestFactory.create(AppModule);
app.use(logger);
//or app.use(cron())
await app.listen(3000);
//doesn't allow dependency injection
//or - this if you want dependency injection
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*'); <--- the main thing
}
}