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

    View Slide

  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

    View Slide

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

    View Slide

  4. MAIN IDEA

    View Slide

  5. Understand how
    NestJS works
    under the hood

    View Slide

  6. View Slide

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

    View Slide

  8. HISTORY

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. DECORATORS

    View Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

  23. DTO

    View Slide

  24. View Slide

  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

    View Slide

  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

    View Slide

  27. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. DTO
    EXAMPLE

    View Slide

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

    View Slide

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

    View Slide

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

  35. Microservices

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  39. APP
    LYFECYCLE

    View Slide

  40. View Slide

  41. View Slide

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

    View Slide

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

    View Slide

  44. CTX,
    Execution
    context

    View Slide

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

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

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

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

  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 {
    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 Slide

  50. QUESTIONS
    TIME!

    View Slide