My journey in NestJS starts from here
Mohammad Jawad barati
Software Engineer | Fullstack Engineer | Microservices | TS/JS | Python | IaC | AWS | TDD | BDD | Automation | CI/CD
Why NestJS happens to be?
Because of Node.js echo system. What is Node.js echo system?
First, you have to know what is NestJS, Here is the most simplest definition- Obviously IMO:
Jargons and concepts in NestJS
You can use @Get(), @Post(), etc to specify the endpoint HTTP method. In the parenthesis you can pass a string as the pas for that endpoint. e.x. @Get('/users/:id'). One quick help and very useful in developing a good RESTful API, use :id when the id is resource id - In other word do this if necessary @Get('/users/:postId').
DTOs will be used to validate input and specify the response schema. In a more mature system our DTOs should be mapped to the database and specify which fields should be selected and how they should be returned to the client.
In DTOs we also tends to keep our eyes on user's role to prevent exposing too much data. In general we will have a generic mapper function for each resource. Then we have custom DTOs for each endpoint which says which fields should be selected from database and it should be in the response if user has X role.
Used in authentication and authorization. We do have access to the execution context in the guards. Guards is executed after all middlewares, before pipes, interceptors, and controllers. Guards have to implement CanActivate interface.
For example for a simple RBAC you can define a custom decorator named Roles which accepts needed roles to access that endpoint and then another guard which check if the user/jwt payload has those roles.
We can apply guards globally, or endpoint level, or controller level. In the global one we can ignore then those endpoint/controllers with attaching extra data to the request and use it in the guard. read this stackoverflow Q&A.
DI comes into play. Any provider should be annotated with @Injectable decorator and listed in the providers. We usually do create a controller, and multiple service, just to have a neat, modular, SOLID codebase. In other word for sake of separation of concerns we do use MVCS architecture.
In NestJS we do follow singleton design pattern which implies that providers usually are alive as long as our app is up. But we can also create request scope provider. The other important notion in providers is that you should export soly those where you need them in other modules.
When you faced with circular dependency use forwardRef function -?forwardRef(() => UsersModule) - in imports instead of normal imports. The other portion is that your code should not rely on which constructor would be called in this scenario.
Gather close application domains in one module.
Methods where runs/interpose just before the controller. Most common examples: app.useGlobalPipes where adds global pipelines and @UsePipe where only works for that controller. Usually we use them for validation, and transforming. e.x.
// Controller level pipelines
@Param('id', ParseIntPipe)
@Param('id', new ParseIntPipe(/*In case that you wanna do specific configuration*/))
// Global level pipelines
app.useGlobalPipes(new ValidationPipe(), new ValidationPipe())
providers: [
{ provider: APP_PIPE, useClass: ValidationPipe }
]
Run the following command in the terminal to create a new App
nest new app-name
Configure tsconfig:
then you have to install the following packages in your app
pnpm add helmet csurf winston express-winston nest-winston cookie-parser class-validator class-transformer passport passport-local passport-jwt @nestjs/passport @nestjs/jwt?@nestjs/config argon2
In case you have MongoDB as your database:
npm i mongoose mongodb @nestjs/mongoose
Installing development dependencies in your app, some DefinitelyTyped packages, and also some other packages:
npm i -D @types/cookie-parser @types/csurf @types/passport-jwt
Now you have to config the @nestjs/config package to read the env from a .env files or from the OS exported envs. As I love to keep my `app.module.ts` file as clean as is possible I wrote the way I separate the different environmental variables in different config files:
mkdir src/configs
touch src/configs/auth.config.ts src/configs/web-app.config.ts src/configs/winston.config.ts src/configs/mongodb.config.ts
Ignore each one that does not make sense to you. Now just as an example, you can see my mongodb.config.ts:
You will see how we can use this envs in action a little down. Now just let me to complete ConfigModule setup. Open the app.module.ts and add the following code in the imports section:
With the isGlobal conf we lessen the burden of importing ConfigModule for each module. Obviously if you wanna introduce each module's conf in it this config is not so handy.
Let's use @nestjs/config package:
Now we can config our MongoDB, open the app.module.ts file, and add the following code in the imports:
领英推荐
As you can see I would like to config the mongoose asynchronously. The entered string defined in the mongodb.config.ts file. And this is how you can use the ConfigService in the other modules. First, you have to inject them via DI and then use them.
Now it's time to define our entities. note that we define entities for each module like this:
As you see I define some nested schemas to make my entity definition cleaner. And this is how you can define nested schemas:
Now it is time for introducing our models to the mongoose. So we need to go to the created module and in its `imports` add the following codes:
I like one thing here, your collection name would be completely explicit. Because we set it With the `Test.name`.
You install it before. now its time to use it as a global middleware in the main.ts, It is not that simple. you need to pass its configs to it too. Or you can out its configurations in another file in configs directory like CORS.
app.use(helmet({
? ? contentSecurityPolicy: true,
? ? crossOriginEmbedderPolicy: true,
? ? crossOriginOpenerPolicy: true,
? ? crossOriginResourcePolicy: true,
? ? dnsPrefetchControl: true,
? ? expectCt: true,
? frameguard: true,
? ? hidePoweredBy: true,
? ? hsts: true,
? ? ieNoOpen: true,
? ? noSniff: true,
? ? originAgentCluster: true,
? ? permittedCrossDomainPolicies: true,
? ? referrerPolicy: true,
? ? xssFilter: true,
}));
Cross-origin resource sharing (CORS) is a mechanism that allows resources to be requested from another domain.?Now I wanna config the CORS in a separated file, in the configs directory:
Now we will config the cors in the main.ts in its bootstrap function:
const { corsConfigs } = corsConfigsGenerator();
app.enableCors(corsConfigs);
Now we wanna protect our app against CSRF attacks. As my experience shows me I have to define a middleware to handle it:
and another middleware for the errors:
Now we can config csurf in the `bootstrap` function:
You also have to config the cookie-parser as well:
app.use(cookieParser());
Now assume you wanna config the port of your NestJS app. It should be read from the env file. So we can use ConfigService like this in the main.ts in the bootstrap function:
const configService = app.get(ConfigService);
const appConfigs = configService.get<webAppConfigs>(
'webAppConfigs',
);
// ...
await app.listen(appConfigs.port);
You can access other configured envs in this way in the `main.ts`
Now its time to configure our Winston, So in the `src/configs/winston.config.ts` write the following configuration:
Now we have to config it in the `app.module.ts`:
and if you wanna change the whole NestJS logger you can add the following configurations in the `main.ts` file into the `bootstrap` function to change it to the wisnton:
const nestWinston = app.get(WINSTON_MODULE_NEST_PROVIDER);
app.useLogger(nestWinston);
If you need a very simple JWT based authentication you can clone my repo from the this GitHub repositiry & checkout to this commit sha:
1af64a56f6f41c4b9b7ccb3aa9df624658874310
You need to create a new module with the `nest g module auth` and then you can start defining the JWT guard in the guards' directory:
Now you can implement multiple strategies as your business needed. In this case I just want to define the JWT strategy:
If you had a mindset like me you need to define the decoded type of the JWT token in the types directory as I did:
And in the `auth.module.ts` we move our last chess:
To make it works we need to import the auth module in the `app.module.ts`, Therefore I import the auth module in the `app.module.ts` and add it in the imports:
AuthModule
Now we can create other modules...
keep calm and you can see the final result here in my repo
Any question? feel free to ask, As I could I will answer you.
From Iran, Afghan developer. Kasir barati.