Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deep Dive Into NestJS at FWDays

Deep Dive Into NestJS at FWDays

Avatar for Nikita Galkin

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. Why NestJS? ▰ Inspired by Angular ▰ Freedom within the

    Frame ▰ Dependency injection ▰ Big adoption and ecosystem ▰ Documentation ▰ Declarative code ▰ TypeScript first
  5. ▰ Node.js ☞ http server ▰ Express ☞ routing and

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

    handling ▰ ACL and ctx specific DB queries ▰ routing ▰ API spec generation ▰ testing ▰ code organization
  7. 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 }; } }
  8. 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 }; } }
  9. 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;
  10. class Messenger { async getMessages() { try await api.getData() //

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

    getMessages() { await api.getData() // <-- can throw custom ServerError } }
  12. 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) ); }
  13. 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) ); }
  14. 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) ); }
  15. 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 }); });
  16. 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; });
  17. DTO

  18. 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
  19. 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
  20. 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();
  21. 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; }
  22. 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; }
  23. 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); } }
  24. 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; } }
  25. 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); };
  26. 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 }; } }
  27. 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 }; } }
  28. NestJS microservices: ▰ Remote Procedure Call ▰ ReqRes or PubSub

    patterns ▰ DTOs ▰ ACLs ▰ Many protocols (TCP, Redis, etc) ▰ All the same things as for HTTP
  29. 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(); } }
  30. 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();
  31. 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; } }
  32. 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; });
  33. 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; }
  34. 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; }
  35. 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]; } }