1. |
|
---|---|
2. |
|
3. |
|
4. |
|
5. |
|
6. |
|
7. |
|
8. |
|
9. |
|
10. |
As we’ve seen throughout the course so far. In NestJS - applications everything has its place.
This "structured - application" organization, helps us mange complexity and develop with SOLID principles.
This Type of organization is, especially useful as the size of our application or team grows. Keeping code organized, establishing clear boundaries, and having dedicated architectural building blocks, all with the separate of responsibilities. Help us make sure that our application remain easily maintainable and scalable over time.
In NestJS we have 4 additional "building blocks" for features, that we haven’t showcased yet!. These are:
Exception Filters are responsible for handling and processing "unhandled - exception" that might occur in our application. They let us control the "Exact - flow" and "Content - flow" of any or specific Responses, we send back to the client.
Pipes are typically useful to handle 2 - things:
-
Transition, meaning to transform "input - data" to "desired output", and
-
Validation, meaning to "evaluate input data" and if VALID - let it pass through the "Pipe" unchanged. But if "NOT - VALID", throwing an Exception.
Guards determine whether a given Request meets certain condition, like "authentication", "authorization", "roles", "ACLs[1]", etc. And if the conditions are met, the requests will be allowed to access the route.
Interceptor have many useful capabilities inspired by the "Aspect Oriented Programming[2] - technique". Interceptors make it possible to:
-
Bind extra logic, before or after method execution.
-
Transform the result returned from a method.
-
Extend "basic - method" behavior.
-
Completely "override a method", depending on specific conditions. For example: handling something like "caching - responses".
So now that we’ve covered the basics. Let’s dive into all 4 these new building blocks in the next few lessons.
Before we jump into the specifics of each Nest "building - block". Let’s take a step back and talk about a few approaches we can take to bind any of these "building - blocks", to different parts of our application.
Basically there are 3 different ways of binding to our "route - handlers" with a bonus "4th" way that specific to (Pipes).
-
Filters
-
Guards
-
Interceptors
-
Pipes
Nest "building - blocks" can be:
-
Globally - scoped
-
Controller - scoped
-
Method - scoped
-
Param - scope, which said is available to Pipes only.
Note
|
These different "binding - techniques" give you granularity and control at different levels in you application. |
Each one does NOT override another, but rather "layers each one - "on top".
So be careful on how you implement these.
For example, if you have a globally-"scoped - Pipe", it will be applied as well as any other (Pipe) you might add. Whether it’s "controlled - scoped", "method - scoped", etc.
So far in this course we’ve already seen globally-"scoped - pipes" in action, when we use the "ValidationPipe" to helps us validate incoming - "request - payloads", amongst other things.
If we open up our "main.ts"
- file.
// main.ts
import { NestFactory } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes( // <<<
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
await app.listen(3002);
// console.log("app is run on port: 3002");
}
bootstrap();
We’ll see that we previously bound the "ValidationPipe" globally by calling
"useGlobalPipes()"
- method of our ""app"
- instance".
You could see that if we type app.use
"intellisence" shows us corresponding
methods for every other "building - block" available here. Respectively
"useGlobalPipes()"
, "useGlobalGuard()"
, "useGlobalInterceptors()"
,
and "useGlobalFilters()"
, etc..
Going back to our "ValidationPipe()"
here. One big limitation of setting it up
and instantiating it by ourselves like this, is that we can NOT - "inject any
dependencies" here!. Since we’re setting it up outside of context of any
"NestJS - Module".
So how do we work around this?.
One option we have, is to set up a "Pipe" directly from inside a "Nest - Module" using the "custom - provider" based syntax, we saw in earlier lessons.
Let’s open up our "AppModule" - file, and define something called the "APP_PIPE"
- Provider.
// app.module.ts
import { Module, ValidationPipe } from "@nestjs/common";
import { APP_PIPE } from "@nestjs/core";
...
...
@Module({
imports: [
...
...
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_PIPE, // <<<
useClass: ValidationPipe, // <<<<
},
],
})
export class AppModule {}
This "APP_PIPE"
- "provider" is a "special - TOKEN" exported from
"@nestjs/core"
- packages.
Providing - "ValidationPipe"
in this manner. Let’s Nest instantiate the
"ValidationPipe"
within the scope of the "AppModule" and once created,
registers it as a "Global Pipe".
Note that there are also "corresponding - tokens" for every other "building
- block" feature!, such as "APP_INTERCEPTOR"
, "APP_GUARD"
, and
"APP_FILTER"
.
Back to our "ValidationPipe"
. What if we don’t want to use it globally? But
some are more specific like on a "certain - Controller".
Let’s imagine that we want to bind a "ValidationPipe"
to every - "route
handler" defined only within our "CoffeesController".
Let’s open up our "CoffeesController" - file and make use a new decorator
"@UsePipes()"
that we haven’t seen yet.
// coffees.controller.ts
import { Controller, Get, Param, Body, Post, Patch, Delete, Query, Inject, UsePipes, ValidationPipe } from "@nestjs/common";
@UsePipes(ValidationPipe) // <<<
@Controller("coffees")
export class CoffeesController {
constructor(
private readonly coffeesService: CoffeesService,
@Inject(REQUEST)
private readonly request: Request,
) {
console.log("[!!] CoffeesController created");
}
...
...
}
This @UsePipes()
decorator can be passed in a "single - Pipe Class" or
a "comma separated list of Pipe - Classes". Just like in other scenarios.
There are also "corresponding - decorators" for every other "building - block"
that can be used here as well. Named "@UseInterceptors()"
,
"@UseGuards()"
, and "@UseFilters()"
.
// coffees.controller.ts
import { Controller, Get, Param, Body, Post, Patch, Delete, Query, Inject, UsePipes, ValidationPipe } from "@nestjs/common";
@UsePipes(new ValidationPipe()) // <<<
@Controller("coffees")
export class CoffeesController {
constructor(
private readonly coffeesService: CoffeesService,
@Inject(REQUEST)
private readonly request: Request,
) {
console.log("[!!] CoffeesController created");
}
...
...
}
Alternatively, you can even pass an "instance" of class here. Take for example
providing "new ValidationPipe()"
inside of the decorator.
This is super useful when you want to pass in a specific - "configuration
object" to the "ValidationPipe"
for this exact scenario.
Note
|
As the best practice, try to apply "filters' by using "classes" instead of "instances" whenever possible. |
This best practice "reduces memory usage" since Nest can easily reuse instances of the "same class", across your entire Module.
All "building - blocks" can also be "Method - scoped". Imagine that you want to
bind a "Pipe" to a "specific - Route". We can achieve this by simply applying
the same decorator we just saw "@UsePipes()"
, but on top of the specific method we
want to declare it on.
Let’s say we want to add "specific validation" to our GET - findALL()
- method,
within "CoffeesController".
// coffees.controller.ts
import { Controller, Get, Param, Body, Post, Patch, Delete, Query, Inject, UsePipes, ValidationPipe } from "@nestjs/common";
@Controller("coffees")
export class CoffeesController {
constructor(
private readonly coffeesService: CoffeesService,
@Inject(REQUEST)
private readonly request: Request,
) {
console.log("[!!] CoffeesController created");
}
@UsePipes(new ValidationPipe()) // <<<
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
return this.coffeesService.findAll(paginationQuery);
}
...
...
}
With this setup, This "ValidationPipe"
is only applied to this single findALl()
- "Route - handler".
We are already familiar with the 3 different ways of tying "filters", "guards", "pipes", and "Interceptors" to our "Route - handlers". But as we said, there is a "4th" bonus way - that’s only available to "Pipes", and it’s called "Param-based scope".
"Param-scoped Pipes", are useful when the "validation - logic" concern ONLY ONE "specific parameter".
Let’s scroll down to the update()
- method.
// coffees.controller.ts
import { Controller, Get, Param, Body, Post, Patch, Delete, Query, Inject, UsePipes, ValidationPipe } from "@nestjs/common";
@Controller("coffees")
export class CoffeesController {
constructor(
private readonly coffeesService: CoffeesService,
@Inject(REQUEST)
private readonly request: Request,
) {
console.log("[!!] CoffeesController created");
}
@UsePipes(new ValidationPipe()) // <<<
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
return this.coffeesService.findAll(paginationQuery);
}
...
...
@Patch(":id")
update(@Param("id") id: string, @Body(ValidationPipe) updateCoffeeDto: UpdateCoffeeDto) { // <<<
return this.coffeesService.update(id, updateCoffeeDto);
}
}
This method takes "2 - arguments": the "resource - id", as well as the "payload" required to update the existing entity.
What if we want to bind a "Pipe" to the "body" of the request but not the "id - parameter". This is exactly where the "param - based" - Pipe comes in handy.
By passing the "ValidationPipe" - class reference, directly to the `"@Body"
- decorator here, we can let Nest know to run this particular - Pipe
-exclusively for just this specific parameter!, and there we have it.
With these 4 powerful "building - blocks". We can now control the "flow", "content", "validation" on anything in our application, globally, all the way down to a specific "controller", "method", or even a "parameter".
NestJS comes with a built-in Exception "layer", responsible for processing all "unhandled - Exceptions" across our application. When Expection is NOT handled by our application, it is automatically caught by this "layer*, which send the appropriate user-friendly response.
Out of the box. This action is performed by a built-in "global - ExceptionFilter". While this base built-in "ExceptionFilter" can automatically handle many use cases for us. We may want "full control" over it.
For example, we may want to add exception "logging" or "return" our Errors back in a different "JSON - schema". "Exception - Filters" are designed for exactly this purpose!.
They let us be in charge of the exact "flow of control" and the "content" of the Response being sent back to the client.
Let’s create an "ExceptionFilter", that is responsible for "catching exception" that are an instance of the "HttpException" - Class, and implement our own custom "response - logic" for it.
Let’s get started by firing up our terminal, and generating a "filter" - class using the NEST - CLI "filter - schematic", by entering:
$ nest g filter common/filters/http-exception
CREATE src/common/filters/http-exception.filter.spec.ts (201 bytes)
CREATE src/common/filters/http-exception.filter.ts (195 bytes)
Note that we generated this "filter" in a "/common/"
- directory, where we
can keep things that are not tied to any specific domain.
Let’s open up he newly generated "HttpExceptionFilter", and see what we have inside.
// http-exception-filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
@Catch(HttpException) // <<<
export class HttpExceptionFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {}
}
As you can see, the NEST - CLI generated and example "filter", without any "business logic" of course.
The "@Catch()"
- decorator on top, binds the "required metadata" to the
"ExceptionFilter". This "@Catch()"
- decorator can take a "single
- parameter" or a "comma separated list".
This allows us to set up a "filter" for several "types of exceptions" at once if we want it.
Since we want to process all exceptions that are instances of "HttpException".
Let’s pass the "HttpException" - class between the parentheses "()"
.
All "ExceptionFilter’s" should implement the
"ExceptionFilter[3]"
- interface exported from
@nestjs/common
. This interface requires that we provide the "catch()"
- method with its indicated "method signature". Also we can see that our
class accepts a "Type - Argument", which indicates the Type of the "exception
argument" in our "catch()"
- method.
Again, since we want to process all exceptions that are instances of
a "HttpException"
. Let’s changes this to "<T extends HttpException>"
.
// http-exception-filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common";
import { Response } from "express";
@Catch(HttpException)
export class HttpExceptionFilter<T extends HttpException> implements ExceptionFilter { // <<<
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
const error = typeof response === "string" ? { message: exceptionResponse } : (exceptionResponse as object);
response.status(status).json({
...error
})
}
}
All right, with all of that setup now, we can implement our custom - "response
- logic". To do this, we’ll need to access the "underlying - platform"
- "Response{}"
Object so that we can manipulate or transform it and CONTINUE
sending the response - afterwards.
So where we get a hold of the "original Response"?
Let’s use the second parameter here, "host:"
which we can see as an instance
of "ArgumentsHost"
, and call the method "switchToHttp()"
on it. Saving this
as the variable "ctx"
, short for "context". This "switchToHttp()"
- method
gives us access to the native in-flight "Request or Response Objects". Which
is exactly what we need here.
Next, let use this "ctx"
- variable, and call the "getResponse()"
- method
on it. This method will return our "underlying platforms" - Response.
Note
|
Remember in NestJS, this is ExpressJS by default, but could also be swapped for Fastify. |
For better "Type - safety" here. Let’s specify the "Type" as a "<Response>"
,
importing this Type from the "express"
- packages.
Now we have our "Response". Let’s use the "exception - parameter" available to
us in this - method, and extract "2 - things". The "statusCode"
, and
"body"
from the "current - exception".
To get the "status", we can simply call the "getStatus()"
- method as we see
above.
Let’s also get a hold of a "raw - "exceptionResponse"
", by calling to
"getResponse()"
- method and saving that variable as well.
Since for demonstration purposes - we’re trying to pass back this *original - "Error - Response", we need to do a little bit of work here.
First we need to test whether the Response is a String or an Object. If it’s a string, we’re going to create an Object an put that String inside of the "message - property". Otherwise we’re all set in our "exceptionResponse" is already an Object.
By doing of this. Our Errors will now be fairly "uniform", and we can ("…"
)
spread this "error - variable" into our "final - response", which we’ll do in
a moment!.
Great, so now that we have everything we need. Let’s start building our "response" back that we’ll be sending.
First, let’s set the "StatusCode" for the response we’re going to send back via
the "status()"
- method ("response.status(status)"
).
Lastly, we need to send the "exceptionResponse"
back. Our application
underlying platform is ExpressJS, which is the default. So there are several
ways we could do this!.
In our case. Let’s just use Express’s ".json()"
- method. We ca simply chain
this method after our "status()"
- call (as we see above), and using the
("…"
) "spread - operator", we can pass the original Error from our Exception
inside this ".json()"
- method.
As of right now. Our "ExceptionFilter"
here isn’t really doing anything unique
yet. So let’s pass in something custom here along with the "original
- exception". This way we have something we can look for in all of our "errors"
to make sure everything with the "ExceptionFilter"
works!.
Let’s add a new "timestamp"
- property and give it the value of "new
Date().toISOString()"
.
Great. Now with all this in place, let’s bind this "global
- "ExceptionFilter"
" to our application.
Since we don’t need any "external - providers" here, we can just bind this
"ExceptionFilter - globally" using the " "app"
- instance" in our "main.ts"
- file.
Let’s head over to the "main.ts"
- file and add it real quick with
"app.useGlobalFilters()"
.
// main.ts
import { NestFactory } from "@nestjs/core";
import { HttpException, ValidationPipe } from "@nestjs/common";
import { AppModule } from "./app.module";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
app.useGlobalFilters(new HttpExceptionFilter()); // <<<
await app.listen(3002);
// console.log("app is run on port: 3002");
}
bootstrap();
Now that everything’s in place. Let’s test it out by triggering some "AP
errors!". Let’s make sure that our app is running in our terminal, and if not,
make sure we run: "npm run start:dev"
.
With our applications running. Let’s open up insomnia
, and perform a GET
- request to a "non existing* - resource and purposely make an Error happen!.
Let’s hit someting /coffees/-1
where "-1" is obviously an "id"
we don’t have
in the database.
// request: 'GET - http://localhost:3002/coffees/-1'
// Body - raw: JSON
{}
// response, 404 - NOT FOUND
{
"statusCode": 404,
"message": "Coffee with 'id: #-1' not found",
"error": "Not Found",
"timestamp": "2021-04-06T07:30:31.156Z"
}
As we can see, the Response came back with an Error and clearly used new custom
"ExceptionFilter". Since the response contains our new "timestamp"
- property.
Perfect!.
So naturally this exception we created was a basic example implementation. But you can see that within this "ExceptionFilter", we could have just as easily used some sort of "logging - Service" to track our errors, maybe even called an "Analytics - API". Anything we’d want to do whenever an "HttpException" occurs in our application.
Guards have a single responsibility. Which is to determine whether a "given request" is allowed access to something.
If the "request" meets certain conditions, such as "permissions". "roles", "ACLs.[1]", etc.. It will be allowed access to that route.
If the condition are NOT met, that it will be denied and an Error will be thrown.
One of the best use-cases for "Guard": is "Authentication" and "Authorization".
For example, we could implement a "Guard" that "extract" and "validates a Token", and uses the extracted information to determine whether the "request can proceed or not".
Since there are many different approaches and strategies to handle authentication and authorization. In this lessons we’ll focus on a simplified example and learn how to leverage Guards themselves in our projects.
Note
|
If you’re interested in learning more about Authentication itself, check out our separate Course extension, which particularly focused on implementation of an Enterprise-grade Authentication feature, and all the complexities that go along with that. |
All right. So to learn how to Guard - "work conceptually", let’s create a Guard that is responsible for "2" things:
-
Validating whether an API_KEY is present within an "authorization" - Header.
-
Validating whether the route being accessed is specified as "public".
Let’s call this new - Guard "ApiKeyGuard"
. Let’s fire up the terminal and
generate a "Guard - class" using the Nest - CLI.
$ nest g guard common/guard/api-key
CREATE src/common/guard/api-key.guard.spec.ts (169 bytes)
CREATE src/common/guard/api-key.guard.ts (301 bytes)
Note
|
We generate this Guard in the "/common/" - directory, where we can
keep things that aren’t tied to any specific domain.
|
All right, let’s open up this newly generated "ApiKeyGuard" - file, and see what we have inside.
// api-key.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
As we can see the Nest - CLI generated an example Guard for us, without any
logic inside of course. Similar to Providers. A Guard is just a Class with the
"@Injectable()"
- decorator which we’ve seen in previous lessons.
One important requirement of Guard is, that they should implement the
"canActivate"
- interface exported from @nestjs/common
. This interface
requires us to provide the "canActivate()"
- method within our class.
This "canActivate()"
- method should return Boolean, indicating whether the
"current - request" is allowed to proceed OR denied access. This method can
also "return a Response" that’s either synchronous or asynchronous, such as
a "Promise"
or "Observable"
.
Nest will use the "return - value" to control the next action. If it return
- "true"
: the request will be processed. If it return "false"
: Nest will
deny the request.
Looking at the example code, the Nest - CLI generated for us here. We have
"return true"
hard-coded for now. This means that currently, every request
will be allowed to proceed!.
Just for testing purposes, let’s changes a couple line,
// api-key.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
return false; // <<<
}
}
We change "return true"
to "return false"
, indicating that every request
should be "denied - access".
Now that our initial "Mock Guard" is all set. Let’s bind it to our application "globally".
Let’s open up the "main.ts" - file, and add "AppUseGlobalGuards()"
, passing
in our new "ApiKeyGuard" inside of it.
// main.ts
import { NestFactory } from "@nestjs/core";
import { HttpException, ValidationPipe } from "@nestjs/common";
import { AppModule } from "./app.module";
import { ApiKeyGuard } from "./common/guard/api-key.guard"; // <<<
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
app.useGlobalGuard( new ApiKeyGuard()); // <<<
await app.listen(3002);
// console.log("app is run on port: 3002");
}
bootstrap();
Let’s make sure that our applications is running in the background, and let’s
navigate to insomnia
and test any endpoint.
// request: 'GET - http://localhost:3002/coffees/'
// Body - raw: JSON
{}
// response, 403 - FORBIDDEN
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
As we can see ALL of our endpoints now responds with status "403 Forbidden
Resource"
, just as we expected. It works perfectly.
But right now our Guard is not programmatically determining anything about our
"Route" or the "Caller" yet. It’s simply always returning "false"
.
That doesn’t make much sense right? Instead let’s set up our Guard to handle the scenario we talked about in the beginning of this lesson. Which is to "validate an API_KEY that should be present within each request", but-only-on routes that are NOT specified as "public".
So how can we get started here?.
Well, first let’s define this "API_KEY" that we’re talking about. To make sure that we never push this secret key to our Git - Repo. Let’s define the "API_KEY" as an "environment - variable".
Open up our ".env"
- file that we created in a previous lesson and let’s add
the following "API_KEY"
- line.
// .env
/*
* CAUTION: never SUBMIT or PUSH this crendential '.env' - file in github or track on Git!.
* This only for course and education purpose.
*/
...
...
API_KEY=7AddwM8892Pbsewqaxx00wqaMMzal
Note
|
The "API_KEY" here is just a random generated String, so feel free to use whatever you’d like for this example. |
Within this in place. Let’s head back to our Guard.
Here in our Guard. We want to retrieve the API_KEY from any "incoming - request" that is not labeled as "public". We’ll be handling this "public - part" in a moment.
// api-key.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getResponse<Request>()
const authHeader = request.header("Authorization")
return authHeader === proceess.env.API_KEY;
}
}
For our "API_KEY". Let’s assume that the "caller" is passing this "key" as an
"authorization - header". To get the information about this "HTTP - request".
We’ll need to access it all from the "ExecutionContext" ("contex' - param),
which inherits from "ArgumentsHost"
, which we’ve already familiarized
yourself with when we used it with the "ExceptionFilter".
We can actually use those same "helper - methods" from before, but this time, to get the reference of the "Request - Object" instead of the Response.
This "switchToHttp"
- method, guves us access to the native "in-flight
Request", "Response", and "Next" Objects! Which is exactly what we need.
Next we need to call the "getRequest()"
- method on it, which will return our
underlying platform’s "Request wrapper object".
Remember in Nest this is ExpressJS by default. But you could also be swapped for Fastify.
For better "Type - safety" here. Let’s specify type as a <Request>
importing
this Type from the express
- package again.
Now let’s use this "Request - object" to retrieve the "authorization - header" from each request, if it’s even there.
Lastly, let’s compare the "authorization header" passed in, with the "registered API_KEY" we have stored in our "environment - variable".
For now we’ll simply access the "environment - variables" using the
"process.env"
- Object but ideally you’d want to leverage the
(@nestjs/config
) "ConfigService"
instead.
With all of this in place. Let’s open insomnia
and test any endpoint in our
application, and see if our Guard works so far.
// request: 'GET - http://localhost:3002/coffees/'
// Body - raw: JSON
{}
// response, 403 - FORBIDDEN
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
As we can see our application responded with the status "403 Forbidden
Resource"
.
Since we didn’t pass in any authorization header especially one with our specific "API_KEY". It looks like our Guard is working perfectly.
Let’s try the same API - request again but this time let’s add an "authorization - header" with the "correct - API_KEY".
With these in place. Let’s hit send and call the endpoint again.
// request: 'GET - http://localhost:3002/coffees/'
// Body - raw: JSON
{}
// Header
{
"Authorization": "7AddwM8892Pbsewqaxx00wqaMMzal"
}
// response, 200 - OK
[
{
"id": 1,
"title": "Salemba Roast#1",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": [
{
"id": 1,
"name": "chocolate"
},
{
"id": 2,
"name": "vanilla"
}
]
},
{
"id": 2,
"title": "Salemba Roast#2",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": []
},
{
"id": 3,
"title": "Salemba Roast#3",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": []
}
]
Perfect. We’ve got a "200"
response back and no error this time!.
Our Guard was able to verify that the "Authorization - header" matched our "secret - API_KEY" and we were allowed access to continue the "API - Request".
So far we’ve finished setting up the "API_KEY - validation" - functionality we wanted, but still aren’t checking whether the "specific - route" being accessed is "public" or not.
In the next chapter we’ll look at how Metadata and a few Nest features can help us achieve just that!.
In the last lesson, we implement the first goal for our "new Guard". Which was to verify an "API - Token" is present when a "Route is accessed".
In this lesson, we’ll be looking at how we can complete the next piece of functionality we needed, which was to detect whether the route being access is declared "public" or not.
How can we declaratively specific which endpoints in our application are "public", or any "data" we want stored alongside controllers or routes.
This is exactly where "custom - Metadata" comes into play.
Nest provides the ability to attach "custom - Metadata" to "routes - handlers"
through the "@SetMetadata()"
- decorator. The "@SetMetadata()"
- decorator
takes "two - parameters".
-
First being the "key" that will be used as the "lookup key",
-
Second, is "metadata - value" which can be any Type. This is where we put whatever values we want to store for this "specific - key".
So let’s put this to use, to learn how it all works.
Let’s open our "CoffeesController" - file. Head over to our "findAll()"
- method, and add this @SetMetadata()
- decorator on top. Making sure it’s
imported from the nestjs/common
package.
// coffees.controller.ts
import {
Controller,
Get,
Param,
Body,
Post,
Patch,
Delete,
Query,
Inject,
UsePipes,
ValidationPipe,
SetMetadata // <<<
} from "@nestjs/common";
...
...
@Controller("coffees")
export class CoffeesController {
constructor(
...
...
) {
console.log("[!!] CoffeesController created");
}
@SetMetadata("isPublic", true) // <<<
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
return this.coffeesService.findAll(paginationQuery);
}
...
...
}
Inside the decorator. Let’s pass those "Metadata - Key" and "values - parameters".
For our "key", let’s enter the String of "isPublic"
, for our "value"
let’s enter a Boolean of "true"
.
This is the most bare-bones way of setting up Metadata on a route, but it’s NOT actually the "best practice".
Ideally we should create our own decorator to achieve the same result. This is much better practice, because we will have less duplicated code, we can reuse the decorator in multiple places, and a "custom - decorator" gives us much more "Type - safety".
Let’s improve our existing code and make our "own - decorator", and call it
"Public"
.
First, let’s create a new folder within the "/common/"
- directory and call it
"/decorators/"
. Here we can store any other future decorator we might make.
In this folder. Let’s create a new file called "public.decorator.ts"
.
Let’s open up our new file,
// public.decorator.ts
import { SetMetadata } from "@nestjs/common";
export const IS_PUBLIC_KEY ="isPublic";
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
In this file. We’re going to export 2 things.
-
First, our metadata "key",
-
Second, our new decorator itself, that we’re going to call "Public".
For our "key" let’s exports "const = IS_PUBLIC_KEY"
and set it equal to the
string of "isPublic". Just like we called previously.
The benefit of exporting this variable here is, that anywhere we might look up this metadata, we can now import this variable, instead of using "magic - string" and accidentally mistyping the name.
Now let’s export our "decorator" by typing "export const Public"
, and setting
this equal to an arrow function ("⇒"
) that return "SetMetadata"
.
Inside of "SetMetadata()"
, just like before, we need to pass in "key" and
"value". So let’s use our "IS_PUBLIC_KEY" - variable and for the value let’s
pass in the Boolean of "true"
, just like we did previously.
That’s it!. We just made our first - decorator!.
Next let’s swap out the code we previously added in our "CoffeesController" to
use our "new - decorator". Let’s head back over to that findAll()
- method
signature and replace the previously added @SetMetadata
- expression with this
@Public()
- decorator, making sure import it from our local directory.
// coffees.controller.ts
...
...
import { Public } from "../common/decorators/public.decorator";
@Controller("coffees")
export class CoffeesController {
constructor(
...
...
) {
console.log("[!!] CoffeesController created");
}
@Public()
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
return this.coffeesService.findAll(paginationQuery);
}
...
...
}
Perfect. We now have much more "future - proof" and easily reusable decorator that we can use throughout entire application if needed!.
Let’s tie everything together and fix up that "ApiKeyGuard" to use it.
Currently, our Guard return "true" or "false", depending on whether the
"API_KEY" was provided with the request. But now we need to add our
"isPublic"
- logic here.
We need the Guard to return "true"
, when the "isPublic"
- metadata is found,
before continuing further and testing whether an "API_KEY"
is present.
In order to access the "Routes - Metadata" in our Guard. We’ll need to use a new
"helper - class" called "Reflector"
.
The "Reflector"
- class allows us to retrieve metadata within a specific
context. This class is provided out of the box by the Nest - framework, from
the @nestjs/core
- package.
Let’s inject the "Reflector"
- class here, inside of our
"constructor()"
.
// api-key.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core"; // <<<
import { ConfigService } from "@nestjs/config"; // <<<
import { Observable } from "rxjs";
import { Request } from "express";
import { IS_PUBLIC_KEY } from "../decorators/public.decorator";
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(
private readonly reflector: Reflector, // <<<
private readonly configService: ConfigService, // <<<
) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.get(IS_PUBLIC_KEY, context.getHandler()) // <<<
if (isPublic) {
return true
}
const request = context.switchToHttp().getRequest<Request>();
const authHeader = request.header("Authorization");
return authHeader === this.configService.get("API_KEY");
}
}
Now we can use this Provider within the "CanActivate()"
- method, to retrieve
the metadata of our handler.
Let’s add a new variable called "isPublic"
. With this variable, let’s
utilize "this.reflector.get()"
which looks up metadata by its "key". In our
case the exported variable "IS_PUBLIC_KEY"
, we just made a moment a go.
"Reflector"
is requires a "target object context", for the "second
- parameter", in our case we want to target the "method - handler" in our
"given - context". So let’s pass in "context.getHandler()"
.
Note
|
Just for reference. If you need to retrieve metadata from a "Class
- level", you’d call "context.getClass()" here instead.
|
Note
|
For more information on "Reflector" and other possibilities here. Read
more about it in NestJS documentation
|
Okay. Great. Now that we have the "value" from our decorator inside of our Guard, there’s one last thing we need to do.
If a route is "public". We can simply skip the validation of the "API_KEY"
.
Let’s add a simple if
- statement. If "isPublic"
is "true"
, let’s just
"return true"
.
Lastly, we mentioned earlier that we shouldn’t use "process.env"
directly. So
let’s fix that and instead use the "ConfigService".
Let’s move into the "constructor()"
real quick and inject "ConfigService".
With this "Service - injected", we can now replace the previously used
"process.env.API_KEY"
call, with: this.configService.get()
pasing in the
string of "API_KEY"
.
Let’s save our changes and check the terminal to make sure everything compiling correctly.
$ npm run start:dev
src/main.ts:20:25 - error TS2554: Expected 2 arguments, but got 0.
20 app.useGlobalGuards(new ApiKeyGuard());
~~~~~~~~~~~~~~~~~
src/common/guard/api-key.guard.ts:12:9
12 private readonly reflector: Reflector,
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An argument for 'reflector' was not provided.
[11:18:22 PM] Found 1 error. Watching for file changes.
OK, so our Guard is ready, but it looks like the application couldn’t bootstrap properly, and it is throwing compilation errors!. What happened? Well, this errors appears because we’re using "Dependency Injection" inside of our Guard, which was instantiated in the "main.ts" - file.
So how can we fix this?
As we showed in previous lesson. "Global Guards" that depend on other
"Classes" MUST be registered within a " "@Module"
- context".
Let’s fix this real quick, and add this Guard to a Module; and actually let’s take it up a notch, and create a "new - Module" for our "common" - folder, and we can instantiated our Guard there.
Let’s fire up another terminal window, and let’s generate a Module and call it
"common"
with
$ nest g module common
CREATE src/common/common.module.ts (83 bytes)
UPDATE src/app.module.ts (1775 bytes)
This will generate a "Module - class" where we can register any "global - enhancers" we might make in the future including our "ApiKeyGuard".
Great. So let’s open up our "new - Module" and utilize the "custom - provide" setup we learned in a previous lessons.
// common.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_GUARD } from "@nestjs/core";
import { ApiKeyGuard } from "../common/guard/api-key.guard";
@Module({
imports: [ConfigModule],
providers: [
{
provide: APP_GUARD,
useClass: ApiKeyGuard,
},
],
})
export class CommonModule {}
This is where we pass in an "object" into our " provider
- Array" providing
a specific "Class" or "Key" and then "Value" to be used for it.
Inside of our @Module()
- decorator. Let’s add a providers: []
Array, and
pass in an Object with "provide: APP_GUARD"
and "useClass: ApiKeyGuard"
.
This setup is very similar to using "app.useGlobalGuards()"
that we had in our
"main.ts"
- file. But as we said, that option is only available if our Guard
DO NOT use "Dependency - Injection".
Note
|
There is a way around that, but it isn’t best practice since you’d have to manually pass in dependencies. |
One last thing here. Let’s make sure to import "ConfigModule"
in the "import:
[]"
Array here, so we can use "ConfigService" in our Guard.
Now let’s open up our "main.ts"
- file and remove that "useGlobalGuards"
line, since we don’t need it anymore and save all of our changes.
import { NestFactory } from "@nestjs/core";
import { HttpException, ValidationPipe } from "@nestjs/common";
import { AppModule } from "./app.module";
import { ApiKeyGuard } from "./common/guard/api-key.guard";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
await app.listen(3002);
// console.log("app is run on port: 3002");
}
bootstrap();
Let’s switch to our terminal window again to make sure everything can bootstrap properly now.
$ npm run start:dev
[!] DatabaseModule.register() - instantiated
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [NestFactory] Starting Nest application...
[!!] CoffeesModule - instantiated
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] TypeOrmModule dependencies initialized +57ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] DatabaseModule dependencies initialized +0ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] DatabaseModule dependencies initialized +1ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] ConfigHostModule dependencies initialized +1ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] ConfigModule dependencies initialized +1ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] AppModule dependencies initialized +1ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] CommonModule dependencies initialized +0ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] TypeOrmCoreModule dependencies initialized +154ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] TypeOrmModule dependencies initialized +1ms
[!!] CoffeesService - instantiated
[!!] ConfigService - instantiated | "DATABASE_FOO": - bar
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] CoffeeRatingModule dependencies initialized +2ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [InstanceLoader] CoffeesModule dependencies initialized +0ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [RoutesResolver] AppController {}: +9ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [RouterExplorer] Mapped {, GET} route +6ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [RoutesResolver] CoffeesController {/coffees}: +1ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [RouterExplorer] Mapped {/coffees, GET} route +1ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [RouterExplorer] Mapped {/coffees/:id, GET} route +0ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [RouterExplorer] Mapped {/coffees, POST} route +1ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [RouterExplorer] Mapped {/coffees/:id, PATCH} route +0ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [RouterExplorer] Mapped {/coffees/:id, DELETE} route +1ms
[Nest] 1648627 - 04/07/2021, 1:35:56 AM [NestApplication] Nest application successfully started +4ms
Great. Everything’s is compiling again.
Now, let’s open up insomnia
and put our new Guard to the test. Let’s start by
removing the "Authorization - header", and make a GET /coffees
- request,
which we setup to use our @Public()
- decorator. If everything works as
expected it should not" return a "403 Error"
. Since our Guard *should allow
access to it.
// request: 'GET - http://localhost:3002/coffees/'
// Body - raw: JSON
{}
// Header
{}
// response, 200 - OK
[
{
"id": 1,
"title": "Salemba Roast#1",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": [
{
"id": 1,
"name": "chocolate"
},
{
"id": 2,
"name": "vanilla"
}
]
},
{
"id": 2,
"title": "Salemba Roast#2",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": []
},
{
"id": 3,
"title": "Salemba Roast#3",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": []
}
]
It worked, great!.
Next, let’s try a Route that doesn’t have the @Public()
- decorator, like
a GET - request to the endpoint /coffees/1
. Our Guard should deny us access
to this Route, since it is neither "public" nor are we passing in an
"Authorization - header".
// request: 'GET - http://localhost:3002/coffees/1'
// Body - raw: JSON
{}
// Header
{}
// response, 403 - FORBIDDEN
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
There it is. "403 - Forbidden"
. Our Guard works perfectly!.
So to wrap up, we covered a lot of ground (sublime) in this lesson. Tapping into a lot of concepts we learned in previous lessons.
Now we have additional power to create our "own - decorators", and utilize "handler - Metadata", useful for anything our application might need in the future.
Interceptor have many useful capabilities inspired by the "Aspect Oriented Programming.[2] - technique". This technique aims to increase "modularity" by allowing the separation of "cross-cut" and "concerns".
Interceptor achieve this by adding "additional - behavior" to existing code, without modifying the code itself!.
Interceptor make it possible for us to:
-
Bind extra logic, "before" or "after - method" execution.
-
Transform the "result" returned from a method.
-
Transform the "Exception - thrown" from a method
-
Extend basic "method - behavior"
-
Completely "overriding a method" - depending on specific conditions. For example: doing something like caching - "various - responses".
All right. So to learn how Interceptor "work - conceptually". Let’s learn
through an example use-case where we always want our responses, to be located
within a " "data"
- property" ("data: response_here"
); and create a "new
- Interceptor" to handle this for us.
Let’s call this "new - Interceptor" "WrapResponseInterceptor"
. This new
Interceptor will handle ALL - "incoming - requests", and "WRAP" our data for
us automatically.
To get started. Let’s generate an "Interceptor - class" using the Nest -CLI by entering:
$ nest g interceptor common/interceptor/wrap-response
CREATE src/common/interceptor/wrap-response.interceptor.spec.ts (217 bytes)
CREATE src/common/interceptor/wrap-response.interceptor.ts (318 bytes)
Note
|
We generated this Interceptor in the "/common/" - directory like we
have many times before, since Interceptor isn’t specific to any specific
domain.
|
Let’s open up this newly generated "WrapResponseInterceptor"
- file and see
what we have inside.
// wrap-response.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle();
}
}
The Nest - CLI generated an example Interceptor for us without any "business - logic".
Similar to Providers, an Interceptor is a Class with the "@Injectable()"
- decorator. All Interceptor should implement the NestInterceptor
- interface
exported from @nestjs/common
- package.
This interface requires that we provide the intercept()
- method within our
class. The intercept()
- method should return an "Observable" from the "RxJS
- library".
If you are not familiar with RxJS. It is a library for "Reactive Programming using Observables". Making it easier to "compose - asynchronous" or "callback" - "base code".
RxJS itself is outside of the scope of what we can dive into here. Just know that it’s powerful alternative to "Promises or callbacks".
Back to our code. The "CallHandler"
- interface here implement the
"handler()"
- method ("next.handle()"
), which you can use to invoke the
"Route - handler" - method within your Interceptor.
If you don’t call the "handle()"
- method in your implementation of the
intercept()
- method. The "Route - handler" - method WONT be executed at
all.
This approach means that the intercept()
- method effectively "wrap" the
Request/Response
- stream, allowing us to implement "custom - logic" both
"before" and "after" the execution of the final - "Route - handler".
All right. So we’ve covered a lot of theory so far.
Let’s add some "console.log()'s"
, to see where Interceptors fit in the
"Request/Response" - life-cycle.
// wrap-response.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from "rxjs/operators";
@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log("Before..");
return next.handle().pipe(tap(data => console.log("After..", data)));
}
}
Since we’ve called the console.log()
"before" calling the "next.handle()"
- method.
Our "Before.."
- message should appear in the console BEFORE the actual "Route
- handler" is executed by the framework.
Since "handle" return an RxJS - Observable. We have a wide choice of operators we can use to manipulate the "stream" here.
In this example. We’re using the "tap()"
- operator which invokes an anonymous
logging function upon graceful termination of the "Observable - stream". But
(the tap()
- operator) doesn’t otherwise interfere with the response - cycle
at all.
The "data"
- argument of the arrow function ("⇒"
) and we passed into the
tap()
- operator here, is in fact the response sent back from the "Route
- handler"!.
Basically think of this as whatever comes back from our endpoint!.
Here. We’re just doing a console.log()
to say "After…"
, and logging that
"data"
back as well.
To test it all out. Let’s bind this Interceptor to our application globally.
Let’s open our "main.ts"
- file and add, "app.useGlobalInterceptors()"
,
passing in "new WrapResponseInterceptor()"
.
// main.ts
import { NestFactory } from "@nestjs/core";
import { HttpException, ValidationPipe } from "@nestjs/common";
import { AppModule } from "./app.module";
import { ApiKeyGuard } from "./common/guard/api-key.guard";
import { WrapResponseInterceptor } from "./common/interceptor/wrap-response.Interceptor";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
app.useGlobalInterceptors(new WrapResponseInterceptor());
await app.listen(3002);
// console.log("app is run on port: 3002");
}
bootstrap();
Let’s make sure our application is running in the background and let’s navigate
to insomnia
and test any endpoint in our application.
First let’s make sure we have some "Coffees" stored in our database. If you have "Coffees" on your database you can skip this action.
To do so. Let’s hit the POST /coffees/
- endpoint and add a random "Coffee".
// request: 'POST - http://localhost:3002/coffees/'
// Body - raw: JSON
[
{
"title": "Salemba Roast#1",
"description": null,
"brand": "Salemba Brew",
"flavors": [ "chocolate", "vanilla" ]
},
{
"title": "Salemba Roast#2",
"description": null,
"brand": "Salemba Brew",
"flavors": []
},
{
"title": "Salemba Roast#3",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": []
}
]
// Header
{}
// response, 200 - OK
[
{
"id": 1,
"title": "Salemba Roast#1",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": [
{
"id": 1,
"name": "chocolate"
},
{
"id": 2,
"name": "vanilla"
}
]
},
{
"id": 2,
"title": "Salemba Roast#2",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": []
},
{
"id": 3,
"title": "Salemba Roast#3",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": []
}
]
Perfect. Now let’s hit the GET /coffees/
- endpoint and switch back to
terminal to see those console.log()'s
.
$ npm run start:dev
[Nest] 2646412 - 04/07/2021, 1:25:26 PM [NestApplication] Nest application successfully started +5ms
[!!] CoffeesController created
Before..
After... [
Coffee {
id: 1,
title: 'Salemba Roast#1',
description: null,
brand: 'Salemba Brew',
recomendations: 0,
flavors: [ [Flavor], [Flavor] ]
},
Coffee {
id: 2,
title: 'Salemba Roast#2',
description: null,
brand: 'Salemba Brew',
recomendations: 0,
flavors: []
},
Coffee {
id: 3,
title: 'Salemba Roast#3',
description: null,
brand: 'Salemba Brew',
recomendations: 0,
flavors: []
}
]
As we can see, "Before…"
was logged; and then "After.."
, followed by the
actual "value"
from our "findAll()"
- method, which represents the
/coffees/
- endpoints. It looks like our Interceptor worked!.
Let’s open up insomnia
again, and test another endpoint. Let’s test GET
/coffees/1
- endpoint this time.
// request: 'POST - http://localhost:3002/coffees/'
// Body - raw: JSON
{}
// Header
{}
// response, 200 - OK
{
"id": 1,
"title": "Salemba Roast#1",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": [
{
"id": 1,
"name": "chocolate"
},
{
"id": 2,
"name": "vanilla"
}
]
}
Let’s going back into terminal. This time we should see a "single - Object"
following the "After…"
- log in our terminal.
$ npm run start:dev
[Nest] 2646412 - 04/07/2021, 1:25:26 PM [NestApplication] Nest application successfully started +5ms
[!!] CoffeesController created
Before..
After... Coffee {
id: 1,
title: 'Salemba Roast#1',
description: null,
brand: 'Salemba Brew',
recomendations: 0,
flavors: [
Flavor { id: 1, name: 'chocolate' },
Flavor { id: 2, name: 'vanilla' }
]
}
Great, so far so good.
So now that we could see how Interceptors work and their part in the "Request/Response" - life-cycle.
Let’s implement our "data - wrapper" idea we talked about at the beginning of
this lesson, and "wrap" our Response’s inside of a " data
- property".
To do this, let’s replace the "tap()"
- function with the "map()"
- operator.
// wrap-response.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, map } from "rxjs/operators"; //<<
@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log("Before..");
// return next.handle().pipe(tap(data => console.log("After...", data)))
return next.handle().pipe(map((data) => ({data}))); // <<<
}
}
The map()
- operator takes a "value" from the "stream" and returns
a modified one. Since we wanted to wrap all "Responses" in the "data"
- property. Let’s return an Object "{}"
with a "key/value" of "data"
.
Every time this map()
- function s called, it return a "new - Object" with
a "data"
- property filled with our "original - Response".
Note
|
Remember!, everything we’ve done here is mainly for demonstration purposes. |
Hopefully now you can see the power of Interceptors, and how there’s a potential to do so many other things here, like: "passing down version number", "analytics - tracking", etc..
All right. So let’s save all of our changes open up insomnia
and test out this
Interceptor. Let’s execute any "HTTP - Request" and see what the application
send back to us.
// request: 'GET - http://localhost:3002/coffees/'
// Body - raw: JSON
{}
// Header
{}
// response, 200 - OK
{
"data": {
"id": 1,
"title": "Salemba Roast#1",
"description": null,
"brand": "Salemba Brew",
"recomendations": 0,
"flavors": [
{
"id": 1,
"name": "chocolate"
},
{
"id": 2,
"name": "vanilla"
}
]
}
}
Perfect as we can see, the Response was automatically wrapped in an Object
within the "data"
- property.
So, wrapping up in this lesson we’ve show how Interceptor give us an incredible power to manipulate "Requests" OR "Responses", without changing ANY underlying code.
// main.ts
import { NestFactory } from "@nestjs/core";
import { HttpException, ValidationPipe } from "@nestjs/common";
import { AppModule } from "./app.module";
import { ApiKeyGuard } from "./common/guard/api-key.guard";
import { WrapResponseInterceptor } from "./common/interceptor/wrap-response.Interceptor";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
app.useGlobalInterceptors(new WrapResponseInterceptor());
await app.listen(3002);
// console.log("app is run on port: 3002");
}
bootstrap();
So far we’ve learned how to use Interceptor’s to bind "extra logic" before and after "method execution", as well as automatically logging and transforming results returned from "Route - handers".
Another technique useful for Interceptor is to extend the basic function behavior by applying "RxJS - operators" to the "Response - Stream".
Let’s see this in action with a realistic example.
To help us learn about this concept by example. Let’s imagine that we need to handle "Timeouts" for all of our "Route - requests".
When an endpoint does not return anything after a certain period of time, we need to "terminate" the Request, and send back an "Error - message".
Let’s start by generating another Interceptor, naming it "timeout", and also
placing it in the /common/interceptor
- folder.
$ nest g interceptor common/interceptor/timeout
CREATE src/common/interceptor/timeout.interceptor.spec.ts (196 bytes)
CREATE src/common/interceptor/timeout.interceptor.ts (313 bytes)
Let’s open up this newly generated "TimeoutInterceptor", and add some logic in there.
// timeout.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { Observable } from "rxjs";
import { timeout } from "rxjs/operators";
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(timeout(3000));
}
}
In our example we want to terminate a "Request - handler" after a specific
period of time. So let’s use the timeout()
- operator imported from
"rxjs/operators" to achieve this.
To demo this easily. Let’s set a very quick timeout()
of 3000
- milliseconds. This means that when a request is made, after 3
- seconds,
the Request processing will be automatically cancelled for us.
To test it out. Let’s bind this second Interceptor to our application globally, and since it has no dependencies, we can add this in our "`main.ts"` - file.
// main.ts
import { NestFactory } from "@nestjs/core";
import { HttpException, ValidationPipe } from "@nestjs/common";
import { AppModule } from "./app.module";
import { ApiKeyGuard } from "./common/guard/api-key.guard";
import { WrapResponseInterceptor } from "./common/interceptor/wrap-response.Interceptor";
import { TimeoutInterceptor } from "./common/interceptor/timeout.interceptor";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
app.useGlobalInterceptors(
new WrapResponseInterceptor(),
new TimeoutInterceptor()
);
await app.listen(3002);
// console.log("app is run on port: 3002");
}
bootstrap();
Note
|
We can actually bind - "multiple - Interceptors" here simply by separating them with commas. |
Now to make sure that our Interceptor works properly. Let’s open up the
"CoffeesController" - file and temporarily add a "setTimeout()"
- function in
our findAll()
- method to simulate a very long delay.
// coffees.controller.ts
...
...
@Controller("coffees")
export class CoffeesController {
...
...
@Public()
@Get()
async findAll(@Query() paginationQuery: PaginationQueryDto) {
await new Promise(resolve => setTimeout(resolve, 5000));
return this.coffeesService.findAll(paginationQuery);
}
...
...
}
Let’s set a "timeout" for "5" - seconds, higher than the "3" - seconds we
set in our Interceptor to purposely trigger our TimeoutInterceptor
.
Perfect. Now let’s open up insomnia
and execute this endpoint by making
a Request for GET /coffees
.
// request: 'POST - http://localhost:3002/coffees/'
// Body - raw: JSON
{}
// Header
{}
// response, 500 - Internal Service Error
{
"statusCode": 500,
"message": "Internal server error"
}
We received a "500 Error"
which means our TimeoutInterceptor
worked!.
However the "Error - message" we got back is not really descriptive. It says "Internal server Error". But how we make this message more user-friendly?
Heading back to our TimeoutInterceptor
again. Let’s chain another operator
called catchError()
inside our pipe()
- method.
// timeout.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, RequestTimeoutException } from "@nestjs/common";
import { Observable, TimeoutError, throwError } from "rxjs";
import { timeout, catchError } from "rxjs/operators";
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(3000),
catchError((err) => {
if (err instanceof TimeoutError) {
return throwError(new RequestTimeoutException());
}
return throwError(err);
}),
);
}
}
The RxJS "catchError()"
- operator allows us to catch all exceptions that
occurred within the stream.
In the "callback - function" we provide here we can check if the error throne is
an instance of TimeoutError
. Which is also imported from rxjs
.
If so, we use a utility - function throwError()
from rxjs
to create
a stream which immediately emit an Error for whatever is passed into it.
Since we want our error message to be more specific for this scenario, let’s use
a NestJS - Class called "RequestTimeoutException"
imported from
@nestjs/common
, so we can throw the correct "error message" to our users
letting them know that the Request has actually timed-out.
Great. So let’s save our changes navigate back to insomnia
and test the
endpoint once again.
// request: 'POST - http://localhost:3002/coffees/'
// Body - raw: JSON
{}
// Header
{}
// response, 480 - Request Timeout
{
"statusCode": 408,
"message": "Request Timeout"
}
As we can see. This time we received the 408
- Response with the descriptive
message `"Request Timeout".
So in this lesson we learned how to add additional superpower to our Interceptors along with some new tricks from RxJS. All which are very common scenarios and helpful in most of our NestJS applications.
Pipes have 2 - "typical use-cases"
-
Transformation. Where we transform "input - data" to the desired "output".
-
Validation. Where we evaluate - "input - data" and "if - valid", simply pass it through unchanged. If the data is "not - valid", we want to throw an Exception.
In both cases. Pipes operate on the arguments being processed by a Controller’s - "Route - handler".
NestJS triggers a Pipe just before a method is invoked.
Pipes also receive the arguments meant to be passed onto the method. Any "transformation" or "validation - operation" takes place at-this-time. Afterwards the "Route - handler" is invoked with any potentially transformed arguments.
NestJS comes with several "Pipes - available" out of the box. All from the
@nests/common
- package. For example, "ValidationPipe"
, which we’ve seen in
previous lessons, and "ParseArrayPipe"
, which we haven’t seen but it’s an
extremely helpful Pipe that helps us "parse" and "validate - Arrays".
To learn how we can build our own "custom - Pipes". Let’s create that Pipe
automatically parses any incoming String to an Integer, and let’s call it
"ParseIntPipe"
.
Nest already has a "ParseIntPipe"
that we could use from the @nestjs/common
- library. But let’s create this basic Pipe for learning purpose, top help us
"fully understand the basic mechanics of a Pipe".
Let’s fire up the terminal and generate a "Pipe - class" using the Nest - CLI
"Pipe schematic", placing it in the common/pipe
- folder and calling it
"parse-int"
.
$ nest g pipe common/pipe/parse-int
CREATE src/common/pipes/parse-int.pipe.spec.ts (173 bytes)
CREATE src/common/pipes/parse-int.pipe.ts (224 bytes)
Note
|
We generated this Pipe in the `/common/ - directory again where we can keep things that aren’t tied to any specific domain. |
Let’s open up this newly generated "ParseIntPipe"
- file and see what we have
inside.
// parse-int.pipe.ts
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
As we can see the Nest - CLI generated an example Pipe for us without any logic
inside. Similar to Providers a Pipe is a class with the @Injectable()
- decorator. But all Pipes should implement the "PipeTransform"
- interface
exported from @nestjs/common
.
This interface requires us to provide the "transform()"
method within our
Class. This "transform()"
- method has 2 - parameters.
-
"value": The input value of the currently processed argument before it is received by or "Route - handling" - method.
-
"metadata": The metadata of the currently processed argument.
Whatever "value" is returned from this transform()
- function completely
overrides the previous - "value" of the argument.
So when is this useful?. Consider that sometimes the data - passed from the client, needs to undergo some change, before this data can be properly handled by the "Route - handler" - method.
Another use-case for Pipes would be to provide "default - values". Imagine if we had some "required - data fields" that were missing. We could automatically set these defaults within a Pipe.
"Transformer - Pipes" can perform these functions by interposing[4] the "transformation - function" we create between the "client - Request" - and the "Request - handler". This is merely another example. But the point is to show that there are many powerful things you can do with Pipes.
All right. So back to creating a custom "ParseIntPipe". Let’s start implementing the actual logic.
// parse-int.pipe.ts
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
@Injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: string, metadata: ArgumentMetadata) {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException(`validation failed, "${val} is not an Integer.`);
}
return value;
}
}
First. We can assume that the "input - value" or value
is a String, so we can
change the Type from "any"
to "string"
.
Next. Let’s use the built-in parseInt()
- JavaScript function to try parsing
the "input - String" to an "Integer".
Let’s add some" conditional - logic" here in case we see any errors. If the
return value
is not a number
we can make sure to throw
a "BadRequestException"
, otherwise we can return the "modified - value" which
is now an Integer!.
Within this in place, we can now bind our Pipe to some @Param()
- decorators.
Let’s open up the "CoffeesController" - file and navigate to the findOne()
- method.
// coffees.controller.ts
...
...
@Controller("coffees")
export class CoffeesController {
...
...
@Get(":id")
findOne(@Param("id") id: number) {
console.log("GET ===>", id);
return this.coffeesService.findOne("" + id);
}
...
...
}
Before we do anything here. Let’s add a single console.log()
to log the "id"
- "arguments - value".
Now let’s navigate to insomnia
and execute a GET - request to the
/coffees/abc
endpoint.
// request: 'POST - http://localhost:3002/coffees/abc'
// Body - raw: JSON
{}
// Header
{}
// response, 400 - Internal Server Error
{
"statusCode": 500,
"message": "Internal server error"
}
Note
|
We’re going to pass "abc", a String, instead of a number like "1" or "10". |
Go back to our terminal and see what was logged.
$ npm run start:dev
...
...
[Nest] 3531343 - 04/07/2021, 10:49:17 PM [NestApplication] Nest application successfully started +4ms
[!!] CoffeesController created
Before..
GET ===> NaN
[Nest] 3531343 - 04/07/2021, 10:49:18 PM [ExceptionsHandler] invalid input syntax for type integer: "NaN" +1175ms
QueryFailedError: invalid input syntax for type integer: "NaN"
...
...
As we can see it output "NaN"
or "Not a Number"
. This means that although
our "id"
- "abc"
could not have been parsed to a Number, the findOne()
- method was called with an incorrect argument.
To prevent situations like this. Let’s use our newly created "ParseIntPipe"
to
validate the "incoming - parameter" and in case it’s not parse-able to an
Integer, our Pipe will automatically throw a "validation - exception" for us.
So let’s pass in our ParseIntPipe
as the "second - parameter" to our
@Param("id")
in "CoffeesController".
// coffees.controller.ts
...
...
import { ParseIntPipe } from "../common/pipes/parse-int.pipe";
@Controller("coffees")
export class CoffeesController {
...
...
@Get(":id")
findOne(@Param("id", ParseIntPipe) id: number) {
console.log("GET ===>", id);
return this.coffeesService.findOne("" + id);
}
...
...
}
Save the changes, and test the endpoint again.
// request: 'POST - http://localhost:3002/coffees/abc'
// Body - raw: JSON
{}
// Header
{}
// response, 480 - Request Timeout
{
"statusCode": 400,
"message": "validation failed, \"NaN is not an Integer.",
"error": "Bad Request"
}
Great. As we can see this time, we received a "400 - Error"
with the message
stating that "validation failed"
.
As we said, Nest comes with this Pipe already, but hopefully this example showcases the power of Pipes and how you could implement your own to do a multitude of useful processes for your application.
Middleware is a function that is called before the "Route - handler" and any other "building - blocks" are processed. This includes "Interceptor", "Guards" and "Pipes".
Middleware - function have access to the "Request" and "Response - object", and are not specifically tied to any - method, but rather to a specified "Route - path".
Middleware - function can perform the following tasks:
-
Executing code.
-
Making changes to the Request and Response - objects.
-
Ending the "Request/Response" - cycle.
-
Calling the
"next()"
Middleware - function in the "call - stack".
When working with Middleware, if the current "Middleware - function" does not
end the "Request/Response -cycle". It MUST call the "next()"
- method, which
passes control to the next - "Middleware - function". Otherwise the Request will
be left "-Hanging-", and never complete.
So how can we get started creating our own Middleware?.
Custom Nest Middleware can be implemented in either a "Function" or a "Class".
"Function - Middleware" is "stateless", it can NOT inject dependencies, and doesn’t have access to the "Nest - container".
On the other hand, "Class - Middleware" can rely on "external - dependencies" and "inject - Providers" registered in the same "Module - scope".
In this lesson, we’ll focus on building a "Class - Middleware". But remember you can always use "Functions" to create them as well.
Let’s fire up the terminal, and generate a "Middleware - Class" using the Nest
- CLI and let’s call it "logging"
.
$ nest g middleware common/middleware/logging
CREATE src/common/middleware/logging.middleware.spec.ts (192 bytes)
CREATE src/common/middleware/logging.middleware.ts (199 bytes)
Note
|
We generate this Middleware in the "/common/" - directory, again since
it isn’t tied to any specific domain.
|
Let’s open up this newly generated "LoggingMiddleware"
- file and see what
we have inside.
// logging.middleware.ts
import { Injectable, NestMiddleware } from "@nestjs/common";
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.log("Hi from Middleware!");
next();
}
}
As we can see the Nest - CLI generated an example Middleware without any logic
inside. Similar to Providers, a Middleware is a Class with the @Injectable()
- decorator.
All the Middleware should implement the NestMiddleware
- interface exported
from @nestjs/common
. This interface requires us to provide the "use()"
- method within our class. This method, does not have any special requirements.
Note
|
Just remember to always call the "next()" - function otherwise the
Request will be left hanging.
|
For now we’ll keep things simple, let’s just add a console.log()
before
invoking the "next()"
- function to see how Middleware fits into the
"Request/Response - life-cycle".
With this in place we can now register our newly created Middleware.
As we’ve mentioned earlier, Middleware aren’t specifically tied to any method. We can not bind them in a declarative way using decorators. Instead we bind Middleware to a 'Route - path", represented as a String.
To register our LoggingMiddleware
. Let’s open up our `CommonModule" - file.
// common.module.ts
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_GUARD } from "@nestjs/core";
import { ApiKeyGuard } from "../common/guard/api-key.guard";
import { LoggingMiddleware } from "./middleware/logging.middleware";
@Module({
imports: [ConfigModule],
providers: [
{
provide: APP_GUARD,
useClass: ApiKeyGuard,
},
],
})
export class CommonModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes("*");
}
}
First let’s make sure that our "CommonModule" - class implements the
"NestModule" - interface. This interface requires us to provide the
"configure()
- method, which takes the "MiddlewareConsumer"
as an
argument.
MiddlewareConsumer
provides a set of useful methods to tie Middleware to
specific Routes.
Just to test it out. Let’s apply the "LoggingMiddleware" to "all - Routes",
using the Asterisk (" * "
), or "wildcard - operator".
To add our Middleware. Let’s call the "apply()"
- method on a "consumer"
,
passing in our "LoggingMiddleware"
and then let’s call the ".forRoutes(*)"
- method, passing in the "wildcard Asterisk (" * "
)".
Let’s save our changes, and navigate to insomnia
and perform an HTTP -
Request to any endpoint.
Let’s open the terminal, and see if anything popped up.
$ npm run start:dev
...
...
[Nest] 3831529 - 04/08/2021, 2:23:05 AM [NestApplication] Nest application successfully started +5ms
Hi from Middleware!
[!!] CoffeesController created
Before..
Great we can see `"Hi from Middleware" string in our console.
Now taking a step back to our Middleware "consumer"
. There are several other
ways of tying Middleware to different "Routes - paths".
Let’s go back to "CommonModule" - file.
So far we’ve bound the "LoggingMiddleware" to every route using the "Asterisk
" * "
wildcard". But we can also apply only to Roures with, let’s say the
"coffees"
- prefix if we want.
// common.module.ts
...
...
@Module({
...
...
})
export class CommonModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes("coffees");
~~~~~~~
}
}
We can even restrict Middleware further if we’d like let’s say to a particular
"Request - method" like only for "GET" - methods, by passing an Object
containing the 'Route - path" and "RequestMethod"
// common.module.ts
...
...
@Module({
...
...
})
export class CommonModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes({ path: "coffees", method: RequestMethod.GET });
~~~ ~~~~~~ ~~~~~~~~~~~~~
}
}
Note that, we imported the "RequestMethod"
- Enum we used in previous lessons
to reference desired "Request - method" - Type.
Lastly, we can also "exclude" - certain Routes from having the Middleware
applied with the "exclude()"
- method.
// common.module.ts
...
...
@Module({
...
...
})
export class CommonModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).exclude.forRoutes("*");
~~~~~~~
}
}
This "exclude()"
- method can take a "single - String", "multiple - String" or
"RouteInfo{}"
- Object. Identifying Routes to be excluded.
For example we can apply the Middleware to every Route -except- for those with the ""coffees - prefix" if we wanted.
Now that we’ve looked at a few of the options available to use here. Let’s
revert back to our original "forRoutes()"
- wildcard wince we want to bind
the "LoggingMiddleware" to *every existing endpoint for now.
Let’s open up the "LoggingMiddleware" file again and add some additional functionality in here.
For a fun example, let’s calculate "how long" the entire "Request/Response"
- cycle takes, by using console.time()
.
// logging.middleware.ts
import { Injectable, NestMiddleware } from "@nestjs/common";
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.time("[!!] Request-response time");
console.log("[!!] Hi from Middleware!");
res.on("finish", () => console.timeEnd("[!!] Request-response time"));
next();
}
}
Note
|
This calculation will include the Interceptors, Filters, Guards, "method - handler" etc. That this route may have as well!. |
In this example we’re hooking into the ExpressJS "Response - finish" - event
so we know when our console.timeEnd()
should occur.
Let’s save our changes and navigate to insomnia
.
Now let’s execute a random HTTP - request to our application, and head back to the terminal.
$ npm run start:dev
...
...
[Nest] 529632 - 04/08/2021, 12:55:40 PM [NestApplication] Nest application successfully started +5ms
[!!] Hi from Middleware!
[!!] CoffeesController created
Before..
[!!] Request-response time: 27.496ms
As we can see there’s a new "Request/Response - message" indicating that the
full round-trip took a roughly "27 milliseconds"
.
This was all a basic example, but hopefully this shows you the potential that Middleware brings to an application. For a more realistic use case you could potentially utilize something like what we just created, to log "long lasting methods" to a database, and keep track of how long every API takes to complete.
A lot of NestJS is built around TypeScript language feature called "decorators". Decorators are simply functions that apply logic.
// coffees.controller.ts
...
...
@Patch(":id")
update(@Param("id") id: string, @Body(ValidationPipe) updateCoffeeDto: UpdateCoffeeDto) {
// ~~~~ ~~~~
return this.coffeesService.update(id, updateCoffeeDto);
}
...
...
NestJS provides a set of useful "param - decorators" that you can use together
with the HTTP - "Route - handler", for example "@Body()"
, to extract the
"request.body"
. Or "@Param()"
, to pick a specific "request - parameter".
Additionally we can create our own "custom - decorators".
// coffees.controller.ts
...
...
@Patch(":id")
update(@Req("id") request, @Param("id") id: string, @Body(ValidationPipe) updateCoffeeDto: UpdateCoffeeDto) {
// ~~~~
return this.coffeesService.update(id, updateCoffeeDto);
}
...
...
Let’s imagine that for some reason we want to retrieve the "request.protocol"
from within the "Route - handler". Normally we would need to inject the entire
"Request - Object" wit the @Req()
- decorator into method definition.
However this makes this particular method harder to test since we would need to mock the entire "Request - Object" every time we try to test this method.
In order to make our code more readable and easier to test. Let’s create a custom
@Param()
- decorator instead.
To get started let’s open the /common/decorators
- folder, and create
a "protocol.decorator.ts"
inside.
// protocol.decorator.ts
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const Protocol = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.protocol;
}
)
Now inside this file, let’s make use of this file, let’s make use of the
"utility - function": "CreateParamDecorator"
imported from @nestjs/common
to
build our "custom - decorator".
Since we’re trying to find the "request.protocol"
for our decorator, we’re going
to need to retrieve it form the "Request - object". We can retrieve this from
"ExecutionContext"
we’ve seen in previous lessons, with switchToHttp()
, and
then calling getRequest()
.
Afterwards let’s simply return request.protocol
and we’re all set.
Great, now to test our decorator. Let’s open up our "CoffeesController" - file
and temporarily use it on the findAll()
- method.
// coffees.controller.ts
...
...
@Controller("coffees")
export class CoffeesController {
constructor(
...
...
) {
console.log("[!!] CoffeesController created");
}
@Public()
@Get()
async findAll(@Protocol() protocol: string, @Query() paginationQuery: PaginationQueryDto) {
// ~~~~~~
console.log(`[!!] Protocol instantiated: "${protocol}"`);
// ~~~~~~~~~~
return this.coffeesService.findAll(paginationQuery);
}
}
...
...
Let’s add a single console.log()
with the method to log out this @Protocol()
- parameter, and see what it gives us.
Let’s save our changes, navigate to insomnia
and execute a GET /coffees
- request to see everything in action.
Now, let’s head back to our terminal.
$ npm run start:dev
...
...
[Nest] 640992 - 04/08/2021, 2:15:20 PM [NestApplication] Nest application successfully started +4ms
[!!] Hi from Middleware!
[!!] CoffeesController created
Before..
[!!] Protocol instantiated: "http"
[!!] Request-response time: 32.686ms
...
...
We could see "http"
, our protocol, was logged in the console. Great it works.
We can also pass "arguments" to our "custom - decorators" if needed. In this
instance our @Protocol()
- decorator is fully stateless, so there’s no
reason to pass in any parameter since there’s nothing to configure.
However, in more sophisticated scenarios where the behavior of our decorator
depends on different conditions, we can pass the "data"
- argument into them.
For example, let’s say that we wanted to pass a 'default - value" to the decorator
we just made. Let’s pass a string of "https"
, between the @Protocol("https")
parentheses.
// coffees.controller.ts
...
...
@Controller("coffees")
export class CoffeesController {
constructor(
...
...
) {
console.log("[!!] CoffeesController created");
}
@Public()
@Get()
async findAll(@Protocol("https") protocol: string, @Query() paginationQuery: PaginationQueryDto) {
// ~~~~~~
console.log(`[!!] Protocol instantiated: "${protocol}"`);
return this.coffeesService.findAll(paginationQuery);
}
}
...
...
To access this "value" from within our @Param()
- decorator factory. We can
use the "data"
- argument, the first argument here we previously hadn’t
touched.
// protocol.decorator.ts
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const Protocol = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
// ~~~~
const request = ctx.switchToHttp().getRequest();
return request.protocol;
}
)
To make our decorator more self explanatory. Let’s change the "argument - name"
to "defaultValue:"
, and set it to type "string"
, for better readability and
Type-safety.
// protocol.decorator.ts
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const Protocol = createParamDecorator(
(defaultValue: string, ctx: ExecutionContext) => {
// ~~~~~~~~~~~
console.log("[!!] Protocol value:", { defaultValue })
const request = ctx.switchToHttp().getRequest();
return request.protocol;
}
)
With this in place. Now let’s see if these "values" are actually coming into our
decorator, and add a single console.log(defaultValue)
to log out this
"default - value" to our terminal.
Again. Let’s make sure to save our changes, navigate to insomnia
and execute
the same GET /coffees
- request. Back to our terminal again.
$ npm run start:dev
...
...
[Nest] 669039 - 04/08/2021, 2:35:09 PM [NestApplication] Nest application successfully started +4ms
[!!] Hi from Middleware!
[!!] CoffeesController created
Before..
[!!] Protocol value: { defaultValue: 'https' }
[!!] Protocol instantiated: "http"
[!!] Request-response time: 32.14ms
We can see the "{ defaultValue: https }"
Object was logged in the console. Great!.