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

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. 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

    View full-size slide

  2. ▰ Cloud Native, k8s, etc
    ▰ Google Cloud Study Jams
    ▰ Case studies
    ▰ Telegram channel, daily
    ▰ Voice chat, weekly
    ▰ Workshops, coming soon

    View full-size slide

  3. Understand how
    NestJS works
    under the hood

    View full-size slide

  4. Why NestJS?
    ▰ Inspired by Angular
    ▰ Freedom within the Frame
    ▰ Dependency injection
    ▰ Big adoption and ecosystem
    ▰ Documentation
    ▰ Declarative code
    ▰ TypeScript first

    View full-size slide

  5. ▰ Node.js ☞ http server
    ▰ Express ☞ routing and middlewares
    ▰ koa ☞ ctx and async/await support
    ▰ Mongoose ODM or Sequelize ORM
    ▰ ...

    View full-size slide

  6. Many inhouse frameworks based on
    ▰ payload validation
    ▰ error handling
    ▰ ACL and ctx specific DB queries
    ▰ routing
    ▰ API spec generation
    ▰ testing
    ▰ code organization

    View full-size slide

  7. ▰ TypeScript 1.5
    ▰ Reflect-metadata
    ▰ Typestack
    ▻ routing controller
    ▻ class-validator
    ▻ class-transformer

    View full-size slide

  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 };
    }
    }

    View full-size slide

  9. 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 };
    }
    }

    View full-size slide

  10. declare type ClassDecorator =
    (target: TFunction) => TFunction | void;
    declare type PropertyDecorator =
    (target: Object, propertyKey: string | symbol) => void;
    declare type MethodDecorator =
    ( target: Object,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor)=> TypedPropertyDescriptor | void;
    declare type ParameterDecorator =
    (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

    View full-size slide

  11. class Messenger {
    async getMessages() {
    try
    await api.getData() // <-- can throw ServerError
    } catch(err) {
    ...
    }
    }
    }

    View full-size slide

  12. import Catch from 'catch-decorator'
    class Messenger {
    @Catch(ServerError, handler)
    async getMessages() {
    await api.getData() // <-- can throw custom ServerError
    }
    }

    View full-size slide

  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 {
    const defaultOptions = { activate: true };
    const calculatedOptions = Object.assign(defaultOptions, options);
    if (!calculatedOptions.activate) return applyDecorators();
    return applyDecorators(
    BaseCron(cronTime, calculatedOptions)
    );
    }

    View full-size slide

  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 {
    const defaultOptions = { activate: true };
    const calculatedOptions = Object.assign(defaultOptions, options);
    if (!calculatedOptions.activate) return applyDecorators();
    return applyDecorators(
    BaseCron(cronTime, calculatedOptions)
    );
    }

    View full-size slide

  15. 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 {
    const defaultOptions = { activate: true };
    const calculatedOptions = Object.assign(defaultOptions, options);
    if (!calculatedOptions.activate) return applyDecorators();
    return applyDecorators(
    BaseCron(cronTime, calculatedOptions)
    );
    }

    View full-size slide

  16. 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();
    return getConnection().
    getRepository(User).findOneOrFail({ id: req.state?.userId });
    });

    View full-size slide

  17. import { createParamDecorator, ExecutionContext } from '@nestjs/common';
    import { Request } from 'express';
    export const UserId = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
    const req = ctx.switchToHttp().getRequest();
    return req.state?.userId;
    });

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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();

    View full-size slide

  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;
    }

    View full-size slide

  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;
    }

    View full-size slide

  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);
    }
    }

    View full-size slide

  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;
    }
    }

    View full-size slide

  25. import { BadRequestException } from '@nestjs/common';
    import { Transform, TransformFnParams } from 'class-transformer';
    export const DateTransformer:
    Parameters[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);
    };

    View full-size slide

  26. Microservices

    View full-size slide

  27. 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 };
    }
    }

    View full-size slide

  28. 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 };
    }
    }

    View full-size slide

  29. NestJS microservices:
    ▰ Remote Procedure Call
    ▰ ReqRes or PubSub patterns
    ▰ DTOs
    ▰ ACLs
    ▰ Many protocols (TCP, Redis, etc)
    ▰ All the same things as for HTTP

    View full-size slide

  30. APP
    LYFECYCLE

    View full-size slide

  31. 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();
    }
    }

    View full-size slide

  32. 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();

    View full-size slide

  33. CTX,
    Execution
    context

    View full-size slide

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

    View full-size slide

  35. import { createParamDecorator, ExecutionContext } from '@nestjs/common';
    import { Request } from 'express';
    export const UserId = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
    const req = ctx.switchToHttp().getRequest();
    return req.state?.userId;
    });

    View full-size slide

  36. export interface ExecutionContext extends ArgumentsHost {
    /**
    * Returns the *type* of the controller class
    * which the current handler belongs to.
    */
    getClass(): Type;
    /**
    * Returns a reference to the handler (method)
    * that will be invoked next in the request pipeline.
    */
    getHandler(): Function;
    }

    View full-size slide

  37. export declare type ContextType = 'http' | 'ws' | 'rpc';
    export interface ArgumentsHost {
    getArgs = any[]>(): T;
    getArgByIndex(index: number): T;
    switchToRpc(): RpcArgumentsHost;
    switchToHttp(): HttpArgumentsHost;
    switchToWs(): WsArgumentsHost;
    getType(): TContext;
    }

    View full-size slide

  38. 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 {
    constructor(args: PubSubContextArgs) {
    super(args);
    }
    getQueue(): string {
    return this.args[0];
    }
    getAttributes(): Record {
    return this.args[1].attributes;
    }
    getMessage(): Message {
    return this.args[1];
    }
    }

    View full-size slide

  39. QUESTIONS
    TIME!

    View full-size slide