Skip to content

cats enhancements

John Biundo edited this page Aug 25, 2019 · 2 revisions

Enhancements to basic Cats app

This chapter doesn't correspond to any official documentation chapter. It provides some additional features and discussion that should help reinforce some of the concepts covered so far.

With this chapter, if you want to follow along, check out the post-overview-start branch and go to the following along section.

If you just want a final version of the project as of the end of the chapter, check out the post-overview-end branch, and proceed to the upon completion section.

Understanding the Request/Response cycle

While most newcomers to Nest have some familiarity with concepts like Express middleware, the concepts of Exception Filters, Pipes, Guards, and Interceptors may be less familiar (unless, for example, you come from an Angular background). The easiest way to think of these is that they are features that borrow heavily from programming models like aspect oriented programming and declarative programming that allow you to more naturally express functionality in a declarative fashion.

Or, put more concretely, they are ways to inject custom code/behavior into a normal Express-like request/response cycle at strategic points.

One thing that helps reinforce this is to visualize the order in which these various "injection points" (they're called "point cuts" in aspect oriented programming) occur in the request/response cycle. To help make that clearer, let's add a bit of "tracing code" that will help show this in the console output.

Tracing output

We're going to build a helper function that will help visualize this. This isn't meant to be production code, so we'll take the shortest, easiest path to get there.

Following along

Create a folder helpers in src/common, and in it, a file debug-helper.ts. Place the following code in that file:

import * as clc from 'cli-color';

export const dbg = (o: any, ...out: any[]) => {
  const formats = {
    middleware: {
      indent: 2,
      symbol: '[M:]',
      color: 'green',
    },
    guard: {
      indent: 4,
      symbol: '[G:]',
      color: 'magenta',
    },
    interceptor: {
      indent: 6,
      symbol: '[I:]',
      color: 'yellow',
    },
    pipe: {
      indent: 8,
      symbol: '[P:]',
      color: 'cyan',
    },
    filter: {
      indent: 10,
      symbol: '[E:]',
      color: 'red',
    },
  };

  let prefix = '';
  let typeFormat = {
    indent: 0,
    symbol: '',
    color: 'white',
  };

  if ('intercept' in o) {
    typeFormat = formats.interceptor;
  } else if ('use' in o) {
    typeFormat = formats.middleware;
  } else if ('canActivate' in o) {
    typeFormat = formats.guard;
  } else if ('transform' in o) {
    typeFormat = formats.pipe;
  } else if ('catch' in o) {
    typeFormat = formats.filter;
  }

  prefix =
    '>'.repeat(typeFormat.indent) +
    ' ' +
    typeFormat.symbol +
    o.constructor.name +
    ' ';

  console.log(clc[typeFormat.color](prefix) + out.join(''));
};

This creates a function dbg() which we can use as a drop-in replacement for console.log() in our middleware, exception filters, pipes, and guards so that they provide a visual cue of where each executes in the request/response cycle.

The following steps are a bit tedious. We're basically updating or adding dbg() in place of console.log(). You can either perform these steps as described, or simply check out the post-overview-end branch and proceed to the upon completion section below.

src/common/interceptors/logging.interceptor.ts:

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

import { dbg } from '../helpers/debug-helper';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    dbg(this, 'Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(tap(() => dbg(this, `After... ${Date.now() - now}ms`)));
  }
}

src/common/guards/role.guard.ts:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { dbg } from '../helpers/debug-helper';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      dbg(this, 'no role metadata');
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const hasRole = () => user.roles.some(role => roles.includes(role));
    dbg(this, 'hasRole: ', hasRole());
    return user && user.roles && hasRole();
  }
}

src/common/interceptors/logging.interceptor.ts:

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

import { dbg } from '../helpers/debug-helper';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    dbg(this, 'Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(tap(() => dbg(this, `After... ${Date.now() - now}ms`)));
  }
}

src/common/interceptors/transform.interceptor.ts

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';

import { dbg } from '../helpers/debug-helper';

export interface Response<T> {
  data: T;
}

@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, Response<T>> {
  intercept(
    context: ExecutionContext,
    next: CallHandler
  ): Observable<Response<T>> {
    dbg(this, 'Before...');

    return next.handle().pipe(
      map(data => (data ? { data } : undefined)),
      tap(() => dbg(this, `After...`))
    );
  }
}

src/common/middleware/logger.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import { dbg } from '../helpers/debug-helper';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    dbg(this, 'Request...');
    next();
  }
}

src/common/pipes/joi-validation.pipe.ts:

import * as Joi from '@hapi/joi';
import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';

import { dbg } from '../helpers/debug-helper';
import * as util from 'util';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
  constructor(private readonly schema: object) {}

  transform(value: any, metadata: ArgumentMetadata) {
    dbg(this, 'input value: ', util.inspect(value));
    dbg(this, 'metadata: ', util.inspect(metadata));

    const { error } = Joi.validate(value, this.schema, { abortEarly: false });

    if (error) {
      dbg(this, 'error: ', util.inspect(error.details));
      throw new BadRequestException('Validation failed');
    }

    return value;
  }
}

src/common/pipes/parse-int.pipe.ts

import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';

import * as util from 'util';
import { dbg } from '../helpers/debug-helper';

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: any, metadata: ArgumentMetadata): number {
    dbg(this, `input value: ${value}`);
    dbg(this, 'metadata: ' + util.inspect(metadata));

    const val = parseInt(value, 10);
    if (isNaN(val)) {
      dbg(this, 'validation failed: not a number');
      throw new BadRequestException('Validation failed');
    }

    dbg(this, 'return value: ' + util.inspect(val));
    return val;
  }
}

src/common/pipes/validation.pipe.ts:

import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

import * as util from 'util';
import { dbg } from '../helpers/debug-helper';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, metadata: ArgumentMetadata) {
    const metatype = metadata.metatype;

    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }

    dbg(this, 'input value: ' + util.inspect(value));
    dbg(this, 'metadata: ' + util.inspect(metadata));

    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      dbg(this, 'error: ' + util.inspect(errors));
      throw new BadRequestException('Validation failed');
    }
    dbg(this, 'return value: ' + util.inspect(value));
    return value;
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

Since you may have been experimenting a bit along the way, let's check a few other things before running some tests. (You can check out branch post-overview-end if you want to run the exact code I'm running).

  • Make sure CatsController has the @UseFilters(HttpExxceptionFilter) turned on. It should look like this:
...
@UseFilters(HttpExceptionFilter)
@UseInterceptors(LoggingInterceptor)
@Controller('cats')
// @UseGuards(RolesGuard)
export class CatsController {
  constructor(private readonly catsService: CatsService) {}
...
  • Add the RolesGuard to the findOne() method. Also, add a console.log() so we can see where the method invocation fits in the request/response cycle. It should look like this:
...
  @UseGuards(RolesGuard)
  @Get(':id')
  findOne(@Param('id', new ParseIntPipe()) id: string) {
    console.log('running findOne()');
    return `This action returns a #${id} cat`;
  }
...
  • Make sure the create() method binds the ValidationPipe. Also, add a console.log() so we can see where the method invocation fits in the request/response cycle. It should look like this:
...
  @Post()
  @Roles('admin')
  @UsePipes(ValidationPipe)
  async create(@Body() createCatDto: CreateCatDto) {
    console.log('running create()');
    this.catsService.create(createCatDto);
  }
...
  • Also, make sure you're running your custom validation pipe, not the built-in one, by ensuring that you import yours, and not the one from @nestjs/common.

Upon completion

At this point, you can use the tracing code we've just implemented to help visualize the Nest request/response cycle, and how Exception Filters, Pipes, Guards, and Interceptors augment the standard Express processing model.

HTTPie requests to trace the request/response cycle

Run the following requests, which have observations below them:

  1. "Normal" create cats.

    http POST :3000/cats name=Fred age:=3 breed='Alley Cat'

    Here's the logging output you should see. Examine it and compare it to the source code to make sure you can understand what you're seeing.

  2. Create cats with bad data.

    http POST :3000/cats name=Fred age:=3

    Here's the logging output you should see.

  3. Make a "normal" request for one cat by id.

    http GET :3000/cats/1

    Here's the logging output you should see.

  4. Make an invalid request for one cat by id.

    http GET :3000/cats/abc

    Here's the logging output you should see.

  5. Test the unhandled exception

    http GET :3000/cats/kaboom

    Here's the logging output you should see.

Fundamentals section of the docs

We're going to skip over the Fundamentals section of the docs for now, and jump right to Techniques. Specifically, the Authentication chapter.

What's next

Next up is the Authentication chapter.