$30 off During Our Annual Pro Sale. View Details »

Deep Dive Into NestJS at FWDays

Deep Dive Into NestJS at FWDays

Nikita Galkin

June 01, 2021
Tweet

More Decks by Nikita Galkin

Other Decks in Programming

Transcript

  1. 1

  2. Nikita Galkin Love and Know: ▰ How to make developers

    and business happy ▰ Technical and process debt elimination Believe that: ▰ Any problem must be solved at the right level ▰ Software is easy. People are hard ▰ A problem should be highlighted, an idea should be "sold", a solution should be demonstrated Links: Site GitHub Twitter Facebook 2
  3. ▰ Cloud Native, k8s, etc ▰ Google Cloud Study Jams

    ▰ Case studies ▰ Telegram channel, daily ▰ Voice chat, weekly ▰ Workshops, coming soon
  4. MAIN IDEA

  5. Understand how NestJS works under the hood

  6. None
  7. Why NestJS? ▰ Inspired by Angular ▰ Freedom within the

    Frame ▰ Dependency injection ▰ Big adoption and ecosystem ▰ Documentation ▰ Declarative code ▰ TypeScript first
  8. HISTORY

  9. ▰ Node.js ☞ http server ▰ Express ☞ routing and

    middlewares ▰ koa ☞ ctx and async/await support ▰ Mongoose ODM or Sequelize ORM ▰ ...
  10. Many inhouse frameworks based on ▰ payload validation ▰ error

    handling ▰ ACL and ctx specific DB queries ▰ routing ▰ API spec generation ▰ testing ▰ code organization
  11. ▰ TypeScript 1.5 ▰ Reflect-metadata ▰ Typestack ▻ routing controller

    ▻ class-validator ▻ class-transformer
  12. DECORATORS

  13. import { Get, Controller, Render } from '@nestjs/common'; import {

    AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() @Render('index') render() { const message = this.appService.getHello(); return { message }; } }
  14. import { Get, Controller, Render } from '@nestjs/common'; import {

    AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() @Render('index') render() { const message = this.appService.getHello(); return { message }; } }
  15. declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction

    | void; declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; declare type MethodDecorator = <T>( target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>)=> TypedPropertyDescriptor<T> | void; declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
  16. class Messenger { async getMessages() { try await api.getData() //

    <-- can throw ServerError } catch(err) { ... } } }
  17. import Catch from 'catch-decorator' class Messenger { @Catch(ServerError, handler) async

    getMessages() { await api.getData() // <-- can throw custom ServerError } }
  18. import { applyDecorators } from '@nestjs/common'; import { Cron as

    BaseCron } from '@nestjs/schedule'; import { CronOptions } from '@nestjs/schedule/dist/decorators/cron.decorator'; function Cron( cronTime: string | Date, options?: CronOptions & { activate?: boolean } ): ReturnType<typeof applyDecorators> { const defaultOptions = { activate: true }; const calculatedOptions = Object.assign(defaultOptions, options); if (!calculatedOptions.activate) return applyDecorators(); return applyDecorators( BaseCron(cronTime, calculatedOptions) ); }
  19. import { applyDecorators } from '@nestjs/common'; import { Cron as

    BaseCron } from '@nestjs/schedule'; import { CronOptions } from '@nestjs/schedule/dist/decorators/cron.decorator'; function Cron( cronTime: string | Date, options?: CronOptions & { activate?: boolean } ): ReturnType<typeof applyDecorators> { const defaultOptions = { activate: true }; const calculatedOptions = Object.assign(defaultOptions, options); if (!calculatedOptions.activate) return applyDecorators(); return applyDecorators( BaseCron(cronTime, calculatedOptions) ); }
  20. import { applyDecorators } from '@nestjs/common'; import { Cron as

    BaseCron } from '@nestjs/schedule'; import { CronOptions } from '@nestjs/schedule/dist/decorators/cron.decorator'; function Cron( cronTime: string | Date, options?: CronOptions & { activate?: boolean } ): ReturnType<typeof applyDecorators> { const defaultOptions = { activate: true }; const calculatedOptions = Object.assign(defaultOptions, options); if (!calculatedOptions.activate) return applyDecorators(); return applyDecorators( BaseCron(cronTime, calculatedOptions) ); }
  21. import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { Request

    } from 'express'; import { getConnection } from 'typeorm'; import { User } from '~/entities/user'; const CurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => { const req = ctx.switchToHttp().getRequest<Request>(); return getConnection(). getRepository(User).findOneOrFail({ id: req.state?.userId }); });
  22. import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { Request

    } from 'express'; export const UserId = createParamDecorator((data: unknown, ctx: ExecutionContext) => { const req = ctx.switchToHttp().getRequest<Request>(); return req.state?.userId; });
  23. DTO

  24. None
  25. class User { constructor(name, surname) { this.name = name; this.surname

    = surname; } } const homerDTO = { name: 'Homer', surname: 'Simpson' }; const homer = new User('Homer', 'Simpson'); homer instanceof User; // true homerDTO instanceof User; // false JSON.stringify(homer) === JSON.stringify(homerDTO); // true
  26. import { plainToClass } from 'class-transformer'; class User { constructor(name,

    surname) { this.name = name; this.surname = surname; } } const homerDTO = { name: 'Homer', surname: 'Simpson' }; const homer = plainToClass(User, homerDTO); homer instanceof User; // true
  27. None
  28. import { ValidationPipe } from '@nestjs/common'; async function bootstrap() {

    const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe({ transform: true, // use class-transformer // class-validator settings whitelist: true, forbidUnknownValues: true, forbidNonWhitelisted: true })); await app.listen(3000); } bootstrap();
  29. import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; export class

    Credentials { @IsEmail({}, { message: 'Please enter a valid email address' }) @IsNotEmpty() email: string; @IsNotEmpty() @IsString() password: string; }
  30. import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; export class

    Credentials { @IsEmail({}, { message: 'Please enter a valid email address' }) @IsNotEmpty() email: string; @IsNotEmpty() @IsString() password: string; }
  31. DTO EXAMPLE

  32. import { Controller, Get, Query } from '@nestjs/common'; import {

    CurrentUser } from '~/blocks/decorators/current-user'; import { User } from '~/entities/user'; import { CalendarQuery } from '~/dtos/calendar.query'; import { CalendarService } from '~/services/calendar.service'; @Controller('/v1/calendar') export class CalendarController { constructor(private calendarService: CalendarService) {} @Get('/') async calendar(@CurrentUser() user: User, @Query() query: CalendarQuery) { return this.calendarService.getUserCalendar(user, query.from, query.to); } }
  33. import { Transform } from 'class-transformer'; import { IsDate, IsOptional

    } from 'class-validator'; import { DateTransformer } from '~/blocks/transformers/date'; export class CalendarQuery { @IsOptional() @IsDate() @Transform(DateTransformer) readonly from: Date; @IsOptional() @IsDate() @Transform(DateTransformer) readonly to: Date; constructor() { this.from = this.from || this.getNowWithDiff(-10); this.to = this.to || this.getNowWithDiff(10); } private getNowWithDiff(diff: number): Date { const date = new Date(); date.setDate(date.getDate() + diff); return date; } }
  34. import { BadRequestException } from '@nestjs/common'; import { Transform, TransformFnParams

    } from 'class-transformer'; export const DateTransformer: Parameters<typeof Transform>[0] = function ({ value }: TransformFnParams) { if (typeof value !== 'string') { throw new BadRequestException(`Validation failed: "${value}" is not date string'`); } if (isNaN(Date.parse(value))) { throw new BadRequestException(`Validation failed: "${value}" is not date string'`); } return new Date(value); };
  35. Microservices

  36. import { Get, Controller, Render } from '@nestjs/common'; import {

    AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() @Render('index') render() { const message = this.appService.getHello(); return { message }; } }
  37. import { Get, Controller } from '@nestjs/common'; import { AppService

    } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} // import { MessagePattern } from '@nestjs/microservices'; @MessagePattern({ cmd: 'hello' }) render() { const message = this.appService.getHello(); return { message }; } }
  38. NestJS microservices: ▰ Remote Procedure Call ▰ ReqRes or PubSub

    patterns ▰ DTOs ▰ ACLs ▰ Many protocols (TCP, Redis, etc) ▰ All the same things as for HTTP
  39. APP LYFECYCLE

  40. None
  41. None
  42. import { Module, OnModuleInit } from '@nestjs/common'; import { SendGridService

    } from '~/services/sendgrid/service'; @Module({ providers: [SendGridService] }) export class MailModule implements OnModuleInit { constructor(private readonly sendGrid: SendGridService) {} async onModuleInit() { await this.sendGrid.checkApiKeyScope(); } }
  43. import { NestFactory } from '@nestjs/core'; import { AppModule }

    from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Starts listening for shutdown hooks app.enableShutdownHooks(); await app.listen(3000); } bootstrap();
  44. CTX, Execution context

  45. import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import {

    Observable } from 'rxjs'; @Injectable() export class RolesGuard implements CanActivate { canActivate( context: ExecutionContext, ): boolean | Promise<boolean> | Observable<boolean> { return true; } }
  46. import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { Request

    } from 'express'; export const UserId = createParamDecorator((data: unknown, ctx: ExecutionContext) => { const req = ctx.switchToHttp().getRequest<Request>(); return req.state?.userId; });
  47. export interface ExecutionContext extends ArgumentsHost { /** * Returns the

    *type* of the controller class * which the current handler belongs to. */ getClass<T = any>(): Type<T>; /** * Returns a reference to the handler (method) * that will be invoked next in the request pipeline. */ getHandler(): Function; }
  48. export declare type ContextType = 'http' | 'ws' | 'rpc';

    export interface ArgumentsHost { getArgs<T extends Array<any> = any[]>(): T; getArgByIndex<T = any>(index: number): T; switchToRpc(): RpcArgumentsHost; switchToHttp(): HttpArgumentsHost; switchToWs(): WsArgumentsHost; getType<TContext extends string = ContextType>(): TContext; }
  49. import { Message, Subscription } from '@google-cloud/pubsub'; import { BaseRpcContext

    } from '@nestjs/microservices/ctx-host/base-rpc.context'; declare type PubSubContextArgs = [string, Message]; export class PubSubContext extends BaseRpcContext<PubSubContextArgs> { constructor(args: PubSubContextArgs) { super(args); } getQueue(): string { return this.args[0]; } getAttributes(): Record<string, string> { return this.args[1].attributes; } getMessage(): Message { return this.args[1]; } }
  50. QUESTIONS TIME!