NestJs-Notes

Nest.js Course

Nest.js Course

Untitled

Untitled

NestJs provides special tools to control the flow.

Untitled

NestJs decorators

Untitled

Validate data

ValidationPipe

1
2
3
4
5
6
7
8
9
10
11
12
13
import { NestFactory } from '@nestjs/core';
import { MessagesModule } from './messages/messages.module';

import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
const app = await NestFactory.create(MessagesModule);

app.useGlobalPipes(new ValidationPipe());

await app.listen(3000);
}
bootstrap();

Validation workflow

Untitled

Service ↔ Repository

Untitled

One services may include multiple repositories.

Dependency Injection

making use of inversion of control with minimal number of instance

Motivation:

→ : dependent on

Untitled

Pipe → MessagesController → Messages Service → Messages Repository

如果我们在 Service constructor 里面 instantiate the repository

那么我们就违反了 inversion of control principle

Untitled

在Briefly 2.0里, 犯了这个错误,所以导致每个类都基于 一个 machine learning model

In this example, we define an interface , in which the interface defines a list of methods that can be performed.

Untitled

Untitled

To make a controller with inversion of control, we need:

1
2
3
4
5
const repo = new Messages Repository();
const service = new MessagesService(repo);
const controller = new MessagesController(service);

const controller = new MessagesController();

With dependency injection, we can minimize the amount of code

DI Container/Injectors

A place to store all classes and their dependencies AND instances that I have created

Internally, when we feed our controller to Nest, it will look over the constructor of the controller and register all dependencies into the DI container. Then it will create such dependency instance so a controller can be created.

The benefit of doing this is that we only create one copy of dependency and share it through controllers (i.e. services, repository)!

Untitled

Untitled

Injectable marks a class to register in container

add these classes into module decorator providers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 export class MessagesService {
messagesRepo: MessagesRepository;
constructor(messagesRepo: MessagesRepository){
this.messagesRepo = messagesRepo;
}
}
//Same as
export class MessagesService {
constructor(public messagesRepo: MessagesRepository){}
}

export class MessagesService {
constructor(private messagesRepo: MessagesRepository){}
}

DI inside a module

Untitled

DI between modules

1
2
3
4
5
6
7
8
9
// power.modules.ts
import { Module } from "@nestjs/common";
import { PowerService } from "./power.service;

@Module({
providers: [PowerService],
exports: [PowerService]
})
export class PowerModule{}

Untitled

About Typeorm

Untitled

To create a entity:

  1. Connect with db at app.module
  2. Create User entity
  3. import TypeOrmModule.forFeature([User]) at User.module
  4. import User at app.module

Connection with SQLite

forRoot means the connection is shared through all modules!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
entities: [], //at last, add User into this list
synchronize: true, // only in dev: automatically update SQL table
}),
UsersModule,
ReportsModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

Auto create repository for user

1
2
3
4
5
6
7
8
9
10
11
12
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])], //auto-create repo
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}

Untitled

validation pipe

1
2
3
4
5
6
7
8
9
10
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // means: only allow those I defined in DTO to be validate
}),
);
await app.listen(3000);
}
bootstrap();

Inject Repo into Service

1
2
3
4
5
6
7
8
9
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
constructor(@InjectRepository(User) private repo: Repository<User>) {}
}

the decorator InjectRepository(User) is needed for the generic type <User>

this.repo.create({email, password}); creates an entity.

this.repo.save(user); persists the data into DB.

Why we need to split them into two is that: if validation is set on the entity directly, we can validate them between create and save.

Hooks

Hooks are basically the Trigger in SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//user.entity.ts
import {
AfterInsert,
AfterRemove,
AfterUpdate,
Entity,
Column,
PrimaryGeneratedColumn,
} from 'typeorm';

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column()
email: string;

@Column()
password: string;

@AfterInsert()
logInsert() {
console.log('Inserted User: ', this.id);
}

@AfterUpdate()
logUpdate() {
console.log('Updated User: ', this.id);
}

@AfterRemove()
logRemove() {
console.log('Removed User: ', this.id);
}
}

Saving User entity (by create - save) instance will trigger Hook, directly saving (only by save) will not trigger hook.

Similar with save() with insert() update()

save() is called with a user entity object, insert() update() is called with a SQL record

Similar to remove() with delete() .

Update / Partial

1
2
3
4
5
6
7
8
async update(id: number, attrs: Partial<User>) {
const user = await this.findOne(id);
if (!user) {
throw new Error('User not found');
}
Object.assign(user, attrs);
return this.repo.save(user);
}

if we want to use entity , we need to first fetch the entity from DB and then update the entity. Object.assign allows attrs to be expanded and overwrites/add all attributes of user object. Then we call save(user)

controller CRUD / parseInt

1
2
3
4
5
6
7
8
9
10
@Get('/:id')
findUser(@Param('id') id: string) {
return this.usersService.findOne(parseInt(id));
}

@Get('/')
findAllusers(@Query('email') email: string) {
return this.usersService.find(email);
}
//... and more, check out code base

Error handling

We cannot handle NotFoundException at the service level because other transmitting protocol such as WebSocket does not support suck error handling. But in our case, we can do so as we do not aim to utilize this service in other protocols at this time.

Exclude attrs from an entity

Untitled

1
2
3
4
5
6
//user.entity.ts
...
@Exclude()
@Column()
password: string;
...
1
2
3
4
5
6
7
8
9
10
//users.controller.ts
@UseInterceptors(ClassSerializerInterceptor)
@Get('/:id')
async findUser(@Param('id') id: string) {
const user = await this.usersService.findOne(parseInt(id));
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}

This approach is not the best because it is not customizable.

A new approach: custom interceptor

Untitled

We can apply our custom interceptor on all routes, controllers, global levels!

Untitled

Untitled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// interceptors/serialize.interceptor.ts
import { NestInterceptor, ExecutionContext, CallHandler, UseInterceptors } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';

// check it must be a class
interface ClassConstructor {
new (...args: any[]): {};
}

//wrap a decorator around a class
export function Serialize(dto: ClassConstructor ) {
return UseInterceptors(new SerializeInterceptor(dto));
}

export class SerializeInterceptor implements NestInterceptor {
constructor(private dto: ClassConstructor ) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
//Here runs before the handler
// console.log('Before...');

//Here runs after the handler but before the response is sent
return next.handle().pipe(
map((data: any) => {
return plainToClass(this.dto, data, {
excludeExtraneousValues: true,
});
}),
);
}
}

// users.controller.ts

//@UseInterceptors(new SerializeInterceptor(UserDto));
@Serialize(UserDto)
@Get('/:id')
async findUser(@Param('id') id: string) {
const user = await this.usersService.findOne(parseInt(id));
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}

Or we can apply to the whole controller

1
2
3
@Controller('auth')
@Serialize(UserDto)
export class UsersController {...}

Authentication

Create a new Auth service is very scalable if we have more auth feature in the future!

Session

To use session

1
npm install @type/cookie-session cookie-session
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
const cookieSession = require('cookie-session');

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
cookieSession({
keys: ['asdasd'], //for encryption purpose
}),
);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // means: only allow those I defined in DTO to be validate
}),
);
await app.listen(3000);
}
bootstrap();
1
2
3
4
5
6
7
8
9
10
11
@Post('/signin')
async signinUser(@Body() body: CreateUserDto, @Session() session) {
const user = await this.authService.signin(body.email, body.password);
session.userId = user.id;
return user;
}

@Post('/signout')
signOut(@Session() session) {
session.userId = null;
}

This directly store session userId

Custom decorator

1
2
3
4
5
6
7
import { ExecutionContext, createParamDecorator } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
(data: never, context: ExecutionContext) => {
return 'hi!';
},
);

data is the params passed to the decorator!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//current-user.decorator.ts
import { ExecutionContext, createParamDecorator } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
(data: never, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest();
return request.currentUser;
},
);

//current-user.interceptor.ts
import {
NestInterceptor,
ExecutionContext,
CallHandler,
Injectable,
} from '@nestjs/common';

import { UsersService } from '../users.service';

@Injectable()
export class CurrentUserInterceptor implements NestInterceptor {
constructor(private usersService: UsersService) {}

async intercept(context: ExecutionContext, handler: CallHandler) {
const request = context.switchToHttp().getRequest();
const { userId } = request.session || {};

if (userId) {
const user = await this.usersService.findOne(userId);
request.currentUser = user;
}

return handler.handle();
}
}

Since the decorator is not part of the dependency injection system, we need to use an interceptor to access UsersService to retrieve the current user based on the stored session userId

If we do not use the decorator we then need to pass @Request() request: Request in the controller method.

Don’t forget to add CurrentUserInterceptor into provider of users. module.ts

We need to apply CurrentUserInterceptor to the controller to make CurrentUser decorator into effect

1
2
3
4
@Controller('auth')
@Serialize(UserDto)
@UseInterceptors(CurrentUserInterceptor)
export class UsersController {}

Or, we can apply interceptors globally!

1
2
3
4
5
6
7
8
9
10
11
12
13
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [
UsersService,
AuthService,
{
provide: APP_INTERCEPTOR,
useClass: CurrentUserInterceptor,
},
],
})
export class UsersModule {}

This allows CurrentUserInterceptor to be globally applied

Handler is same as controller method

Guard

1
2
3
4
5
6
7
8
import { CanActivate, ExecutionContext } from '@nestjs/common';

export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return request.session.userId;
}
}

Q: why we need ExecutionContext there?

A: We need to make sure what kind of protocol we are using!

Test

Untitled

providers array is a list of injectables we want to register in our testing DI container!

1
2
3
4
5
6
7
providers: [
AuthService,
{
provide: UsersService,
useValue: fakeUsersService,
},
],

AuthService will be registered as normally, when it is initialized in the DI container, the container will look over all its dependencies and try to create the instance.

The point comes: the second object means if any injectables or controller need to initialize a service UsersService, then DI container will use fakeUsersService!

1
2
3
4
5
6
//Create a fake copy of the usersService
const fakeUsersService = {
find: () => Promise.resolve([]),
create: (email: string, password: string) =>
Promise.resolve({ id: 1, email, password }),
};

This fakeUsersService object implements several methods that are needed for initializing AuthService!

find() and create() are all async, we need to return Promise

Promise. resolve()immediately return a resolved promise with the given value.

To help TypeScript to infer the needed methods for fakeUsersService

We can use Partial<UsersService> on fakeUsersService

since create() expects to return a user entity, the fake user entity is an object without method such as logRemove etc.. Therefore, we can enforce it to User entity type.

The full code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { Test } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { User } from './user.entity';
import { UsersService } from './users.service';

describe('AuthService', () => {
let service: AuthService;

// For every single test, gets a new AuthService
beforeEach(async () => {
//Create a fake copy of the usersService
const fakeUsersService: Partial<UsersService> = {
find: () => Promise.resolve([]),
create: (email: string, password: string) =>
Promise.resolve({ id: 1, email, password } as User),
};

// this is a DI container
const module = await Test.createTestingModule({
providers: [
AuthService,
{
provide: UsersService,
useValue: fakeUsersService,
},
],
}).compile();
// Create a test Service
service = module.get(AuthService);
});

it('can create an instance of auth service', async () => {
expect(service).toBeDefined();
});
});
1
"test:watch": "jest --watch --maxWorkers=1"

Speed up testing

More test

If two tests require two different methods implementation,

we need to set the fakeUsersService to global scope and tweek the implementation of find() in the respective test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
it('throws an error if user signs up with email that is in use.', async () => {
fakeUsersService.find = () =>
Promise.resolve([
{ id: 1, email: '[email protected]', password: 'qwe' } as User,
]);
expect.assertions(2);
// We expect it to fail in try, and catch allows test to be done
// Jest will assume test is fail if it does not finish in 5 seconds
try {
await service.signup('[email protected]', 'qwe');
} catch (error) {
expect(error).toBeInstanceOf(BadRequestException);
expect(error.message).toEqual('User already exists');
}
});

it('throws an error if user signs in with unused email', async () => {
expect.assertions(2);
try {
await service.signin('[email protected]', 'qwe');
} catch (error) {
expect(error).toBeInstanceOf(BadRequestException);
expect(error.message).toEqual("User doesn't exist");
}
});

Password comparison

Please refer to the github codebase

https://github.com/q815101630/nestJs_mycv/blob/main/src/users/auth.service.spec.ts

Controller testing

Refers to test

https://github.com/q815101630/nestJs_mycv/blob/main/src/users/users.controller.spec.ts

End-To-End Test

problem

A brand new server is created for each test

Untitled

E2E Test skips main.ts, which means there are no pipes, and session

Untitled

Two way to solve this, first is easy, but not nest-way.

Second way is that , we can apply a global pipe and middleware in App Module!

original main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
const cookieSession = require('cookie-session');

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
cookieSession({
keys: ['super'], //for encryption purpose
}),
);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // means: only allow those I defined in DTO to be validate
}),
);
await app.listen(3000);
}
bootstrap();

original app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ReportsModule } from './reports/reports.module';

import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';
import { Report } from './reports/report.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
entities: [User, Report],
synchronize: true,
}),
UsersModule,
ReportsModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

pipe in module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
entities: [User, Report],
synchronize: true,
}),
UsersModule,
ReportsModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_PIPE,
useValue: new ValidationPipe({
whitelist: true,
}),
},
],
})
export class AppModule {}

This means, if the app module needs APP_PIPE (which automatically runs through each request) ,then use the validation pipe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//app.module.ts

...
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
cookieSession({
keys: ['super'], //for encryption purpose
}),
)
.forRoutes('*');
}
}

Downside of doing all this is that we migrate our pipe, middleware setting into app.module which is not very clear what we are doing with pipe and middleware

I would prefer the first simple method…then. For detail ,seeing the video

repeat test problem

We need two DB, one for development, one for testing.

To achieve this, we need environment variable

Environment Variable

Nest environement variable is incredibly complicated, but we shall still use it in this case of learning purpose.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//app.module.ts
const cookieSession = require('cookie-session');

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV}`,
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => {
return {
type: 'sqlite',
database: config.get<string>('DB_NAME'),
synchronize: true,
entities: [User, Report],
};
},
}),
// TypeOrmModule.forRoot({
// type: 'sqlite',
// database: 'db.sqlite',
// entities: [User, Report],
// synchronize: true,
// }),
UsersModule,
ReportsModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_PIPE,
useValue: new ValidationPipe({
whitelist: true,
}),
},
],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
cookieSession({
keys: ['super'], //for encryption purpose
}),
)
.forRoutes('*');
}
}

Now we have two seperate database for development and test

We can delete the test database for each test suit!

To seperate each test, we need also to close their connection!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//test/jest-e2e.json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"setupFilesAfterEnv":["<rootDir>/setup.ts"]
}

//test/setup.ts
import { rm } from 'fs/promises';
import { join } from 'path';
import { getConnection } from 'typeorm';

global.beforeEach(async () => {
try {
await rm(join(__dirname, '..', 'test.sqlite'));
} catch (e) {}
});

global.afterEach(async () => {
try {
await getConnection().close();
} catch (e) {}
});

More e2e test:

https://github.com/q815101630/nestJs_mycv/blob/main/test/auth.e2e-spec.ts

Association

  • Many to One
  • One to Many
  • Many to Many
  • One to One

Many to One will cause the change

  • First argument is to wrap a class with a function so that circular dependency can be solved.
  • Second argument is a issue of TypeORM: we need to state how the instance can go from the associated object back to itself.
1
2
3
4
5
6
7
//user.entity
@OneToMany(()=> Report, (report)=> report.user)
reports: Report[];

//report.entity
@ManyToOne(() => User, (user) => user.reports)
user: User;

Untitled

fetching a user will not automatically fetch the reports

fetching a report will not automatically fetch the user.

Authorization/ Middleware

Untitled

Interceptor is special because it can perform before and after the handler.

However, if we use something defined in interceptor stage in guard stage, we will have an issue .

Therefore, we need to turn the currentUser interceptor into a middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
declare global {
namespace Express {
interface Request {
currentUser?: User;
}
}
}

@Injectable()
export class CurrentUserMiddleware implements NestMiddleware {
constructor(private usersService: UsersService) {}

async use(req: Request, res: Response, next: NextFunction) {
const { userId } = req.session || {};
if (userId) {
const user = await this.usersService.findOne(userId);
req.currentUser = user;
}
next();
}
}

1
2
3
4
5
6
//users.module.ts
export class UsersModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(CurrentUserMiddleware).forRoutes('*');
}
}

We then need to apply into into user module

Besides, since in our query or param @Param("xxx") and @Query() in either Patch, put, get, it will parse all either string or number into string. therefore, we need to transform them in dto

1
2
3
4
@Get()
getEstimate(@Query() query: GetEstimateDto) {
console.log(query);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export class GetEstimateDto {
@IsString()
make: string;

@IsString()
model: string;

@Transform(({ value }) => parseInt(value))
@IsNumber()
@Min(1900)
@Max(2050)
year: number;

@Transform(({ value }) => parseInt(value))
@Min(0)
mileage: number;

@Transform(({ value }) => parseFloat(value))
@IsLongitude()
lng: number;

@Transform(({ value }) => parseFloat(value))
@IsLatitude()
lat: number;
}

createQueryBuilder

This is just another way of building SQL, I am thinking to use another ORM though.

To Production

Several things we need to take care on the path to production

Environment variable

Take advantage of ConfigService, we can inject ConfigService in anywhere we need and call this.configService.get("xxx")

Synchronize

Type-orm synchronize with the database regarding entities. It may cause losing data during production.

Therefore, we shall use Migration File to explicitly dictate what we want to do with DB change.

Migration


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!