Angular's Architecture on the server: NestJS

  • PhD. Computer Science
  • Computer Science Teacher
  • Web Developer
  • Computer Science Student

Contents

What is NestJS?

https://github.com/nestjs/nest

“Nest is a framework for building efficient, scalable Node.js web applications.”

NestJS

  • It is built with TypeScript and combines elements of
    • OOP (Object Oriented Programming)
    • FP (Functional Programming)
    • FRP (Functional Reactive Programming).
  • Nest makes use of Express, allowing for easy use of the myriad third-party plugins which are available.

Features

  • Syntax similar to Angular.
  • based on well-known libraries (Express / socket.io).
  • Dependency Injection.
  • Modular - defines an easy to follow module definition pattern.
  • Reactive microservice support with message patterns.
  • Exception layer - throwable web exceptions with status codes, exception filters.
  • Pipes - synchronous & asynchronous.
  • Interceptors - built on top of RxJS.
  • Guards - attach additional logic in a declarative manner.
  • Testing utilities (both e2e & unit tests).

Create a new project

                        
                            $ git clone https://github.com/nestjs/typescript-starter.git heroes
                            $ cd heroes
                            $ npm install
                            $ npm start 
                        
                    

Frontend: Heroes Tour

                        
                                $ cd heroes-tour
                                $ npm install
                                $ npm start
                        
                    

Heroes Tour: Dashboard

Heroes Tour: List of Heroes

Controllers

                        
                                import { Get, Controller } from '@nestjs/common';
                                import { Hero } from './interfaces/hero.interface';
                                
                                @Controller('heroes')
                                export class AppController {
                                  private HEROES: Hero[] = [];
                                  @Get()
                                  heroes(): Hero[] {
                                    return this.HEROES;
                                  }
                                }
                        
                    

Add cors and bodyparser express middleware in server

                        
                            $ npm install cors bodyparser @types/cors
                        
                    
                            server.ts
                        
                                import { NestFactory } from '@nestjs/core';
                                import { ApplicationModule } from './modules/app.module';
                                import * as bodyParser from 'body-parser';
                                import * as cors from 'cors';
                                
                                async function bootstrap() {
                                    const app = await NestFactory.create(ApplicationModule);
                                    app.use(cors());
                                    app.use(bodyParser.json());
                                    await app.listen(3000);
                                }
                                bootstrap();
                        
                    

CRUD: Heroes I

                        
                                @Get()
                                heroes(): Hero[] {
                                  return this.HEROES;
                                }
                                @Get(':id')
                                findById( @Param('id') id: string): Hero {
                                  const heroID = parseInt(id, 10); //Pipe
                                  return this.HEROES.find(hero => hero.id === heroID);
                                }
                                @Get('search/:name')
                                findByName( @Param('name') name: string): Hero[] {
                                  return this.HEROES.filter(hero => hero.name.includes(name));
                                }
                                
                            

CRUD: Heroes II

                            
                                    @Delete(':id')
                                    deleteById( @Param('id') id: string): Hero {
                                        const heroID = parseInt(id, 10);
                                        const heroIndex = this.HEROES.findIndex(hero => hero.id === heroID);
                                        const hero = this.HEROES[heroIndex];
                                        this.HEROES.splice(heroIndex, 1);
                                        return hero;
                                    }
                                    @Post()
                                    createHero( @Body('name') name: string) {
                                        const hero: Hero = {
                                            name,
                                            id: this.FAKE_ID,
                                        };
                                        this.HEROES.push(hero);
                                        this.FAKE_ID = this.FAKE_ID + 1;
                                        return hero;
                                    }
                                    
                                

CRUD: Heroes III

                        
                                        @Put()
                                        updateById( @Body('id') id: string, @Body('name') name: string): Hero {
                                            const heroID = parseInt(id, 10);
                                            const hero = this.HEROES.find(hero => hero.id === heroID);
                                            if (hero) {
                                                hero.name = name;
                                            }
                                            return hero;
                                        }
                        
                    

Request object

Nest Decorators Plain express object
@Request() req
@Response() res
@Next() next
@Session() req.session
@Param(param?: string req.params / req.params[param]
@Body(param?: string) req.body / req.body[param]
@Query(param?: string) req.query / req.query[param]
@Headers(param?: string) req.headers / req.headers[param]

Components

Almost everything is a component (Service, Repository, Factory, ...) and they can be injected into controllers or to another components.

Create a component: HeroesService

                        
                                import { Component } from '@nestjs/common';
                                import { Hero } from './interfaces/hero.interface';
                                
                                @Component()
                                export class HeroesService {
                                  private FAKE_ID = 21;
                                  private readonly heroes: Hero[] = [];
                                
                                  findAll(): Hero[] {}
                                  findById(heroID: number): Hero {}
                                  findByName(name: string): Hero[] {}
                                  updateById(heroID: number, name: string): Hero {}
                                  deleteById(heroID: number): Hero {}
                                  create(name: string): Hero {}
                                }
                        
                    

Inject Service in Controller

                            
                                    @Controller('heroes')
                                    export class AppController {
                                      constructor(private readonly heroesService: HeroesService) { }
                                      @Get()
                                      heroes(): Hero[] {
                                        return this.heroesService.findAll();
                                      }
                                      @Get(':id')
                                      findById( @Param('id') id: string): Hero {
                                        const heroID = parseInt(id, 10); //Pipe
                                        return this.heroesService.findById(heroID);
                                      }
                                      @Get('search/:name')
                                      findByName( @Param('name') name: string): Hero[] {
                                        return this.heroesService.findByName(name);
                                      }
                            
                        

Add Service in module

                                
                                        import { Module } from '@nestjs/common';
                                        import { AppController } from './app.controller';
                                        import { HeroesService } from './heroes.service';
                                        
                                        @Module({
                                          modules: [],
                                          controllers: [AppController],
                                          components: [
                                            HeroesService,
                                          ],
                                        })
                                        export class ApplicationModule { }
                                
                            

Modules

Every Nest application has at least one module. In most cases you'll have several modules, each with closely related set of capabilities.
The @Module() decorator takes the single object whose properties describe the module.
components the components that will be instantiated by the Nest injector and may be shared at least across this module.
controllers the set of controllers which have to be created
modules the list of imported modules that export the components which are necessary in this module
exports the subset of components that should be available in the other modules

HeroesModule

                        
                                import { Module } from '@nestjs/common';
                                import { HeroesService } from './heroes.service';
                                import { HeroesController } from './heroes.controller';
                                
                                @Module({
                                  modules: [],
                                  controllers: [
                                    HeroesController,
                                  ],
                                  components: [
                                    HeroesService,
                                  ],
                                })
                                export class HeroesModule { }
                        
                    

AppModule

                            
                                    import { Module } from '@nestjs/common';
                                    import { HeroesModule } from './heroes/heroes.module';
                                    
                                    @Module({
                                      modules: [
                                        HeroesModule,
                                      ],
                                      controllers: [],
                                      components: [],
                                    })
                                    export class ApplicationModule { }
                            
                        

Middlewares

  • The middleware is a function, which is called before route handler.
  • Middleware functions have an access to the request and response objects, so they can modify them.

Cors Middleware

                                    
                                            import { Middleware, NestMiddleware } from '@nestjs/common';
                                            import * as cors from 'cors';
                                            
                                            @Middleware()
                                            export class CorsMiddleware implements NestMiddleware {
                                              public resolve(
                                                options?: cors.CorsOptions,
                                              ): (req: any, res: any, next: any) => void {
                                                return cors(options);
                                              }
                                            }
                                    
                                

Json Middleware

                                        
                                                import { Middleware, NestMiddleware } from '@nestjs/common';
                                                import { json } from 'express';
                                                
                                                @Middleware()
                                                export class JSONMiddleware implements NestMiddleware {
                                                  public resolve(): (req: any, res: any, next: any) => void {
                                                    return json();
                                                  }
                                                }
                                        
                                    

App Module

                        
                                const allRoutes = {
                                    method: RequestMethod.ALL,
                                    path: '*',
                                  };
                        
                                                   
                                                          export class ApplicationModule implements NestModule {
                                                          
                                                            public static corsOptions: CorsOptions | undefined = undefined;
                                                            public static jsonOptions: any | undefined = undefined;
                                                          
                                                            configure(consumer: MiddlewaresConsumer): void {
                                                              consumer.apply(CorsMiddleware)
                                                                .with(ApplicationModule.corsOptions)
                                                                .forRoutes(allRoutes)
                                                                .apply(JSONMiddleware)
                                                                .with(ApplicationModule.jsonOptions)
                                                                .forRoutes(allRoutes);
                                                            }
                                                          }
                                                
                                            

Exceptions

In Nest there's an exceptions layer, which responsibility is to catch the unhandled exceptions and return the appropriate response to the end-user.

HttpException

                           
                        findById(heroID: number): Hero {
                            const hero = this.heroes.find(hero => hero.id === heroID);
                            if (!hero) {
                                throw new HttpException('Hero not found', HttpStatus.NOT_FOUND);
                            }
                            return hero;
                        }
                    
                      
                               
                            findByName(name: string): Hero[] {
                                const heroes = this.heroes.filter(hero => hero.name.includes(name));
                                if (!heroes.length) {
                                    throw new NotFoundException('Heroes not found');
                                }
                                return heroes;
                            }
                        
                    

CustomExceptions

                               
                            import { NotFoundException } from '@nestjs/common';
                            
                            export class NotHeroException extends NotFoundException {
                                constructor(heroID: number) {
                                const msg = `The Hero: ID - ${heroID} not found`;
                                super(msg);
                                }
                            }
                        
                          
                                   
                            import { ForbiddenException } from '@nestjs/common';
                            import { Hero } from '../../heroes/interfaces/hero.interface';
                            
                            export class RepeatHeroException extends ForbiddenException {
                                constructor(hero: Hero) {
                                const msg = `The Hero: ${hero.id} - ${hero.name} is repeated`;
                                super(msg);
                                }
                            }
                            
                        

CustomExceptions

                                   
                                deleteById(heroID: number): Hero {
                                    const heroIndex = this.heroes.findIndex(hero => hero.id === heroID);
                                    if (heroIndex === -1) {
                                        throw new NotHeroException(heroID);
                                    }
                                    const hero = this.heroes[heroIndex];
                                    this.heroes.splice(heroIndex, 1);
                                    return hero;
                                }
                            
                              
                               
                                const heroFound = this.heroes.find(hero => hero.name === name);
                                if (heroFound) {
                                    throw new RepeatHeroException(heroFound);
                                }
                                ...
                                
                            

Pipe

A pipe transforms the input data to a desired output. Also, it could overtake the validation responsibility, since it's possible to throw an exception.

ParseIntPipe - I

                          
                        import { PipeTransform, Pipe, ArgumentMetadata, HttpStatus } from '@nestjs/common';
                        import { NotHeroException } from '../exceptions/not-hero.exception';
                        
                        @Pipe()
                        export class ParseIntPipe implements PipeTransform<string> {
                          async transform(value: string, metadata: ArgumentMetadata) {
                            const val = parseInt(value, 10);
                            if (isNaN(val)) {
                              throw new NotHeroException(val);
                            }
                            return val;
                          }
                        }
                    
                    

ParseIntPipe - II

                       
                        @Get(':id')
                        findById( @Param('id', new ParseIntPipe()) id: number): Hero {
                            return this.heroesService.findById(id);
                        }
                        @Put()
                        updateById( @Body('id', new ParseIntPipe()) id: number, 
                                    @Body('name') name: string): Hero {
                            return this.heroesService.updateById(id, name);
                        }
                        @Delete(':id')
                        deleteById( @Param('id', new ParseIntPipe()) id: number): Hero {
                            return this.heroesService.deleteById(id);
                        }
                     
                    

ValidationHeroPipe - I

                           
                                import { Pipe, PipeTransform, ArgumentMetadata, BadRequestException, HttpStatus } from '@nestjs/common';
                                import { validate } from 'class-validator';
                                import { plainToClass } from 'class-transformer';
                                
                                @Pipe()
                                export class ValidationHeroPipe implements PipeTransform<any> {
                                  async transform(value, metadata: ArgumentMetadata) {
                                    const { metatype } = metadata;
                                    if (!metatype || !this.toValidate(metatype)) return value;
                                    const object = plainToClass(metatype, value);
                                    const errors = await validate(object);
                                    if (errors.length > 0) 
                                        throw new BadRequestException('Validation failed');
                                    return value;
                                  }
                                
                                  private toValidate(metatype): boolean {
                                    const types = [String, Boolean, Number, Array, Object];
                                    return !types.find((type) => metatype === type);
                                  }
                                }
                         
                        

ValidationHeroPipe - II

                               
                                    import { IsString, MinLength, MaxLength } from 'class-validator';
                                    export class HeroDto {
                                      @MinLength(2)
                                      @MaxLength(16)
                                      @IsString()
                                      readonly name: string;
                                    }
                             
                            
                                  
                                        @Post()
                                        create( @Body('', new ValidationHeroPipe()) hero: HeroDto): Hero {
                                            return this.heroesService.create(hero.name);
                                        }
                                
                            

Guards

  • Guards have a single responsibility.
  • They determine whether request should be handled by route handler or not.
  • RolesGuard: One of the best guards use-cases is the role-based authentication.

RolesGuard - I

                              
                                    import { Guard, CanActivate, ExecutionContext } from '@nestjs/common';
                                    import { Reflector } from '@nestjs/core';
                                    
                                    @Guard()
                                    export class RolesGuard implements CanActivate {
                                      constructor(private readonly reflector: Reflector) { }
                                    
                                      canActivate(req, context: ExecutionContext): boolean {
                                        const { parent, handler } = context;
                                        const roles = this.reflector.get<string[]>('roles', handler);
                                        if (!roles) {
                                          return true;
                                        }
                                        req.user = {  /* Fake user's rol */
                                          rol: 'user',
                                        };
                                        const user = req.user;
                                        const hasRole = () => !!roles.find((role) => user.rol === role);
                                        return user && user.rol && hasRole();
                                      }
                                    }
                        
                        

RolesGuard - III

                      
                    @UseGuards(RolesGuard)
                    @Controller('heroes')
                    export class HeroesController {
                       ...
                    
                        @Get()
                        heroes(): Hero[] {}
                        
                        @Get(':id')
                        findById( @Param('id', new ParseIntPipe()) id: number): Hero { }
                        
                        @Roles('user')
                        @Get('search/:name')
                        findByName( @Param('name') name: string): Hero[] {}
                    
                            

RolesGuard - IV

                          
                            @Roles('admin')
                            @Put()
                            updateById( @Body('id', new ParseIntPipe()) id: number, @Body('name') name: string): Hero {}
                            
                            @Roles('admin')
                            @Delete(':id')
                            deleteById( @Param('id', new ParseIntPipe()) id: number): Hero {}
                            
                            @Roles('admin')
                            @Post()
                            create( @Body('', new ValidationHeroPipe()) hero: HeroDto): Hero {}
                        
                                        
                                

Databases

Nest support several ORM's:

  • TypeORM
  • Mongoose
  • Sequelize
                          
                                npm install --save typeorm pg
                                
                        

DatabaseProviders

                              
                                    import { createConnection } from 'typeorm';
                                    export const databaseProviders = [
                                      {
                                        provide: 'DbConnectionToken',
                                        useFactory: async () => await createConnection({
                                          type: 'postgres',
                                          host: 'localhost',
                                          port: 5432,
                                          username: 'root',
                                          password: 'toor',
                                          database: 'heroes',
                                          entities: [
                                            __dirname + '../**/*entity.ts',
                                          ],
                                          synchronize: true,
                                        }),
                                      },
                                    ];
                            
                                            
                                    

DatabaseModule

                                  
                                        import { Module } from '@nestjs/common';
                                        import { databaseProviders } from './database.providers';
                                        
                                        @Module({
                                          components: [...databaseProviders],
                                          exports: [...databaseProviders],
                                        })
                                        export class DatabaseModule { }
                                
                                                
                                        

Repository Pattern

Hero Entity
                                      
                                            import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
                                            
                                            @Entity()
                                            export class Hero {
                                              @PrimaryGeneratedColumn()
                                              id: number;
                                            
                                              @Column({ length: 16, unique: true })
                                              name: string;
                                            }
                                            
                                        

HeroesModule

                                          
                    import { Module } from '@nestjs/common';
                    import { HeroesService } from './heroes.service';
                    import { HeroesController } from './heroes.controller';
                    import { DatabaseModule } from '../database/database.module';
                    import { heroesProviders } from './heroes.providers';
                    
                    @Module({
                      modules: [
                        DatabaseModule,
                      ],
                      controllers: [
                        HeroesController,
                      ],
                      components: [
                        HeroesService,
                        ...heroesProviders,
                      ],
                    })
                    export class HeroesModule { }
                    
                    

Repository Pattern

Heroes Services - I
                                      
                                            @Component()
                                            export class HeroesService {
                                              constructor(
                                                @Inject('HeroesRepositoryToken') 
                                                private readonly heroesRepository: Repository<Hero>,
                                              ) { }
                                            
                                    
                                          
                                            async findAll(): Promise<Hero[]> {
                                                return await this.heroesRepository.find();
                                            }
                                            async findById(heroID: number): Promise<Hero> {
                                                const hero = await this.heroesRepository.findOneById(heroID);
                                                if (!hero) throw new NotHeroException(heroID);
                                                return hero;
                                            }
                                                
                                        

Repository Pattern

Heroes Services - II
                                        
                                      async findByName(name: string): Promise<Hero[]> {
                                        const [heroes, heroesCount] = await this.heroesRepository
                                          .createQueryBuilder('heroes')
                                          .select()
                                          .where('heroes.name like %:name%', { name })
                                          .getManyAndCount();
                                    
                                        if (heroesCount === 0) {
                                          throw new NotFoundException('Heroes not found');
                                        }
                                        return heroes as Hero[];
                                      }
                                    
                                    

Repository Pattern

Heroes Services - III
                                                    
                                      async updateById(heroID: number, name: string): Promise<Hero> {
                                        const hero: Hero = {
                                          name,
                                          id: heroID,
                                        };
                                        await this.heroesRepository.updateById(heroID, hero);
                                        return hero;
                                      }
                                      async deleteById(heroID: number): Promise<Hero> {
                                        const hero = await this.findById(heroID);
                                        await this.heroesRepository.deleteById(heroID);
                                        return hero;
                                      }

                                    
                                    

Repository Pattern

Heroes Services - IV
                                                    
                                      async create(name: string): Promise<Hero> {
                                        try {
                                          await this.heroesRepository.insert({ name });
                                        } catch {
                                          throw new RepeatHeroException({ name } as Hero);
                                        }
                                        return this.findOneByName(name);
                                      }
                                      findOneByName(name): PromiseHero<Hero> {
                                        return this.heroesRepository.findOne({
                                          name,
                                        });
                                    
                

Testing

Nest is easy to test

Nest has a special package @nestjs/testing, which gives a set of utilities to boost the testing process

Heroes Service Testing I

Setup
                        
                                beforeEach(async () => {
                                    testingModule = await Test.createTestingModule({
                                      components: [
                                        HeroesService,
                                        {
                                          provide: "HeroesRepositoryToken",
                                          useFactory: () => ({
                                            find: jest.fn(),
                                            findOneById: jest.fn(() => true),
                                          }),
                                        },
                                      ],
                                    }).compile();
                                
                                    service = testingModule.get(HeroesService);
                                    spyRepository = testingModule.get("HeroesRepositoryToken");
                                  });
                        
                    

Heroes Service Testing II

Some tests
                            
                                    describe("#findById", () => {
                                        it("should return an exception if no hero was found", async () => {
                                          spyRepository.findOneById = jest.fn();
                                    
                                          await expect(service.findById(1)).rejects.toBeInstanceOf(NotHeroException);
                                        });
                                    
                                        it("should not return an exception if a hero was found", () => {
                                          service.findById(1);
                                    
                                          expect(spyRepository.findOneById).toHaveBeenCalledTimes(1);
                                          expect(spyRepository.findOneById).toHaveBeenCalledWith(1);
                                        });
                                      });
                            
                        

Heroes Controller Testing I

Setup
                                
                                        beforeEach(async () => {
                                            testingModule = await Test.createTestingModule({
                                              controllers: [HeroesController],
                                              components: [
                                                {
                                                  provide: HeroesService,
                                                  useFactory: () => ({
                                                    create: jest.fn(),
                                                    findAll: jest.fn(),
                                                  }),
                                                },
                                              ],
                                            }).compile();
                                        
                                            controller = testingModule.get(HeroesController);
                                            spyService = testingModule.get(HeroesService);
                                          });
                                
                            

Heroes Controller Testing II

Some tests
                                
                                        it("#heroes should call to findAll method of heroes service", () => {
                                            controller.heroes();
                                        
                                            expect(spyService.findAll).toHaveBeenCalledTimes(1);
                                          });
                                        
                                          it("#create should call to create method of heroes service", () => {
                                            const heroMocked = { name: "Hero mocked!" } as HeroDto;
                                        
                                            controller.create(heroMocked);
                                        
                                            expect(spyService.create).toHaveBeenCalledTimes(1);
                                            expect(spyService.create).toHaveBeenCalledWith(heroMocked.name);
                                          });
                                
                            

End-to-End Testing

The end to end testing is a great way to verify how the application works from beginning to end.

Heroes E2E Testing I

Setup
                                
                                        const server = express();
                                        server.use(bodyParser.json());    
                                        
                                        beforeEach(async () => {
                                            const testingModule = await Test.createTestingModule({
                                              modules: [HeroesModule],
                                            }).overrideComponent("DbConnectionToken")
                                              .useValue(db)
                                              .overrideGuard(RolesGuard)
                                              .useValue({ canActivate: () => true })
                                              .compile();
                                        
                                            app = testingModule.createNestApplication(server);
                                            agent = request(server);
                                        
                                            await app.init();
                                          });
                                
                            

Heroes E2E Testing

Some tests
                                
                                        it("/GET heroes should return an array of heroes", () => {
                                            return agent
                                              .get("/heroes")
                                              .expect(200)
                                              .expect(({ body: heroes }) => {
                                                expect(heroes).toBeInstanceOf(Array);
                                              });
                                          });
                                        
                                          it("/GET heroes/:id should return a hero", () => {
                                            return agent
                                              .get("/heroes/5")
                                              .expect(200)
                                              .expect(({ body: hero }) => {
                                                expect(hero).not.toBeInstanceOf(Array);
                                              });
                                          });
                                
                            

Thank You