Nest

Bo Vandersteene

@reibo_bo

About me

@reibo_bo

https://medium.com/@bo.vandersteene

https://github.com/bovandersteene

reibo.io

Start a node application

Typescript

routing-controllers

Own database integration

Type-orm

Dependency injection?

Project structure?

nest

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses modern JavaScript, is built with TypeScript (preserves compatibility with pure JavaScript) and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).

Under the hood, Nest makes use of Express, allowing for easy use of the myriad third-party plugins which are available.

const express = require('express')
const app = express()

app.get('/', (req, res) => res.send('Hello World!'))

app.listen(3000, () => console.log('listening on port 3000!'))

Hello world in Express

Hello world in Nest

import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  await app.listen(3000);
}
bootstrap();
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';

@Module({
  imports: [],
  controllers: [AppController],
  components: [],
})
export class AppModule {}
import { Get, Controller } from '@nestjs/common';

@Controller()
export class AppController {
  @Get()
  root(): string {
    return 'Hello World!';
  }
}

Basic application

Application module

Movie Module

Music Module

Book Module

Auth Module

...

Search

Module

Save

Module

...

@Module({
  imports: [
    MusicModule,
    MovieModule,
    BookModule,
    AuthModule
  ],
  controllers: [
    AppController
  ],
  components: []
})
export class ApplicationModule {}

Search module

Search controller

Search service

Movie api

Movie datastore

@Module({
  controllers: [MovieSearchController],
  components: [MovieSearchService],
})
export class MovieSearchModule {
}
@Component()
export class MovieSearchService {
  searchForMovie(key: string, page: number): Observable<TheMovieDbEntity>{
    return Observable.create(observer => {
      const https = require('https');
      const options = {
        hostname: 'api.themoviedb.org',
        path: `/3/search/movie?api_key=${api_key}&page=${page}&query=${key}`,
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      };
      const req = https.request(options, (res: any) => {
        res.setEncoding('utf8');
        res.on('data', (body: any) => {
          if (res.statusCode !== 200) {
            observer.error('Search failed');
            observer.complete();
          } else {
            observer.next(body);
            observer.complete();
          }
        });
      });
      req.on('error', (e: any) => {
        observer.error('Search failed');
        observer.complete();
      });
      req.write('');
      req.end();
    });
  }
}
@Controller('/movie/search')
export class MovieSearchController {

  constructor(private readonly movieSearchService: MovieSearchService) {
  }

  @Get()
  searchFor(@Query('page') page: number = 1,
            @Query('q') key: string): Observable<any> {
    return this.movieSearchService.searchForMovie(key, page);
  }
}

http://localhost:3001/movie/search?q=simpson&page=3

{
  "page": 1,
  "total_results": 45,
  "total_pages": 3,
  "results": [
    {
      "vote_count": 3118,
      "id": 35,
      "video": false,
      "vote_average": 6.9,
      "title": "The Simpsons Movie",
      "popularity": 26.831717,
      "poster_path": "/eCytnEriVur3rT47NWfkgPXD9qs.jpg",
      "original_language": "en",
      "original_title": "The Simpsons Movie",
      "genre_ids": [
        16,
        35,
        10751
      ],
      "backdrop_path": "/gMjtdTP6HIi7CDilqXwnX8vouxO.jpg",
      "adult": false,
      "overview": "After Homer accidentally pollutes the town's water supply, Springfield is encased in a gigantic dome by the EPA and the Simpsons are declared fugitives.",
      "release_date": "2007-07-25"
    },
    {
      "vote_count": 102,
      "id": 116440,
      "video": false,
      "vote_average": 6.8,
      "title": "Maggie Simpson in The Longest Daycare",
      "popularity": 6.654532,
      "poster_path": "/tTlHQ8XzvAkXn5Liptu822rVUvO.jpg",
      "original_language": "en",
      "original_title": "Maggie Simpson in The Longest Daycare",
      "genre_ids": [
        16,
        35
      ],
      "backdrop_path": "/iRq40yPgSkEYrNaD1rOWMhChe07.jpg",
      "adult": false,
      "overview": "Maggie Simpson returns to the Ayn Rand School for Tots. There, Maggie's future prospects are deemed to be minimal, so she is left in one of the daycare's less enjoyable rooms, with just a butterfly for company.",
      "release_date": "2012-07-12"
    },

We don't need all this data

@Interceptor()
export class MovieSearchInterceptor implements NestInterceptor {
  intercept(dataOrRequest, context: ExecutionContext, 
            stream$: Observable<TheMovieDbEntity>)
            : Observable<MovieSearchRespone> {
    return stream$.pipe(
      map(response => ({
        ...response,
        results: response.results.map(movie => this.mapMovie(movie)),
      })),
    );
  }
  private mapMovie(movie: TmbMovie): MovieSearch {
    const { id, title, poster_path, overview } = movie;
    return { id, title, poster_path, overview };
  }
}
@Get()
@UseInterceptors(MovieSearchInterceptor)
searchFor(...): Observable<MovieSearchResponse>

Not only for transform


@Interceptor()
export class LoggingInterceptor implements NestInterceptor {
  intercept(dataOrRequest, context: ExecutionContext,
            stream$: Observable<any>): Observable<any> {
    console.log('Before...');
    const now = Date.now();

    return stream$.pipe(
     tap(() => console.log(`After... ${Date.now() - now}ms`)),
    );
  }
}

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.useGlobalInterceptors(new LoggingInterceptor());
  await app.listen(3001);
}

unlimited possibilities

Setup

Clone & create manual files 

  

 

https://github.com/nestjs/typescript-starter.git

@nest/cli

nest new project-name
nest g module movie
  • controller
  • exception
  • guard
  • interceptor
  • middleware
  • module
  • pipe
  • service

We want to save

const entities = [MovieEntity];
const local: MysqlConnectionOptions = {
  type: 'mysql',
  host: 'localhost',
  port: 3306,
  username: 'xxx',
  password: 'xxx',
  database: 'rbo_collection',
  entities,
  synchronize: false,
  entityPrefix: 'coll_',
  logging: false,
};

@Module({
  imports: [ TypeOrmModule.forRoot(local),
     ... 
})
export class ApplicationModule {}
@Entity()
export class MovieEntity {
  @PrimaryColumn()
  id: number;
  @Column()
  title: string;
  @Column()
  poster_path: string;
  @Column()
  overview: string;
}

Save module

Save controller

Save service

Movie datastore

@Component()
export class MovieStoreService {
  constructor(@InjectRepository(MovieEntity)
              private readonly movieRepsitory: Repository<MovieEntity>) {

  }

  async saveMovie(movie: MovieEntity): Promise<MovieEntity> {
    return await  this.movieRepsitory.save(movie);
  }
}
@Module({
  imports: [TypeOrmModule.forFeature([MovieEntity])],
  controllers: [
    MovieSaveController,
  ],
  components: [
    MovieStoreService,
    MovieSaveService,
  ],
})
export class MovieSaveModule {
}
@Controller('/movie')
export class MovieSaveController {
  constructor(private readonly movieSaveService: MovieSaveService) {
  }

  @Post()
  @UsePipes(new ValidationPipe())
  searchFor(@Body() movie: MovieDto): Promise<MovieEntity> {
    return this.movieSaveService.save(movie);
  }
}

Pipes & body ???

export class MovieDto {
  @IsInt()
  readonly id: number;
  @IsString()
  readonly title: string;
  @IsString()
  @IsOptional()
  readonly poster_path: string;
  @IsString()
  @IsOptional()
  readonly overview: string;
}
@Pipe()
export class ValidationPipe implements PipeTransform<MovieEntity> {
  async transform(value, metadata: ArgumentMetadata) {
    const { metatype } = metadata;
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Validation of movie failed');
    }
    return value;
  }

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

Pipes are global or on a controller

Everyone can access the app & overwrite ?? !!

Security is needed!

Device

Passport

Secure route

Login api

JWT

API + JWT

const adminUser: UserEntity = {
  id: 1,
  firstname: 'Bo',
  lastname: 'admin',
  admin: true,
};
const normalUser: UserEntity = {
  id: 1,
  firstname: 'Bo',
  lastname: 'Vandersteene',
  admin: false,
};

@Component()
export class AuthService {
  async createToken(login: string, password: string) {
    const expiresIn = 60 * 60, secretOrKey = 'secret';
    const user = login === 'admin' ? adminUser : normalUser;
    const token = jwt.sign(user, secretOrKey, { expiresIn });
    return {
      expires_in: expiresIn,
      access_token: token,
    };
  }

  async validateUser(signedUser): Promise<boolean> {
    // put some validation logic here
    // for example query user by id / email / username
    return true;
  }
}
@Component()
export class JwtStrategy extends Strategy {
  constructor(private readonly authService: AuthService) {
    super(
      {
        jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
        passReqToCallback: true,
        secretOrKey: 'secret',
      },
      async (req, payload, next) => await this.verify(req, payload, next),
    );
    passport.use(this);
  }

  public async verify(req, payload, done) {
    const isValid = await this.authService.validateUser(payload);
    if (!isValid) {
      return done('Unauthorized', false);
    }
    done(null, payload);
  }
}
@Controller('/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {
  }
  @Post()
  @HttpCode(HttpStatus.OK)
  async getToken(@Req() request,
                 @Headers('authorization') token: string,
                 @Headers('user-agent') browser: string):
                     Promise<{ expires_in: number, access_token: string }> {
    const b64auth = (token || '').split(' ')[1] || '';
    const [login, password] = new Buffer(b64auth, 'base64').toString().split(':');
    return this.authService.createToken(login, password);
  }
}
@Module({
  components: [
    AuthService,
    JwtStrategy,
  ],
  controllers: [
    AuthController,
  ],
})
export class AuthModule implements NestModule {
  configure(consumer: MiddlewaresConsumer) {
    consumer
      .apply(passport.authenticate('jwt', { session: false }))
      .forRoutes(
        { path: 'movie/*', method: RequestMethod.POST },
      );
  }
}

Guards

import { Guard, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';

@Guard()
export class AdminGuard implements CanActivate {
  canActivate(dataOrRequest, context: ExecutionContext):
             boolean | Promise<boolean> | Observable<boolean> {
      return dataOrRequest.user && dataOrRequest.user.admin;
  }
}
 @Post('admin')
 @UseGuards(AdminGuard)
 @UsePipes(new ValidationPipe())
 saveAdmin(@Body() movie: MovieDto): Promise<MovieEntity> {
   return this.movieSaveService.save(movie);
 }

Use user object!

@Post('admin')
@UseGuards(AdminGuard)
@UsePipes(new ValidationPipe())
saveAdmin(@Body() movie: MovieDto, 
          @User() user: UserEntity): Promise<MovieEntity> {
  console.log(user);
  return this.movieSaveService.save(movie);
}

Run your frontend and backend in one

const allowedExt = ['.js', '.ico', ...];

const resolvePath = (file: string) => path.resolve(`../dist/${file}`);

@Middleware()
export class FrontendMiddleware implements NestMiddleware {
  resolve(...args: any[]): ExpressMiddleware {
    return (req, res, next) => {
      const { url } = req;
      if (url.indexOf(ROUTE_PREFIX) === 1) {
        // it starts with /api --> continue with execution
        next();
      } else if (allowedExt.filter(ext => url.indexOf(ext) > 0).length > 0) {
        // it has a file extension --> resolve the file
        res.sendFile(resolvePath(url));
      } else {
        // in all other cases, redirect to the index.html!
        res.sendFile(resolvePath('index.html'));
      }
    };
  }
}
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewaresConsumer): void {
    consumer.apply(FrontendMiddleware).forRoutes(
      {
        path: '/**', // For all routes
        method: RequestMethod.ALL, // For all methods
      },
    );
  }
}

What was not covered in this talk, but offered

Websockets

Microservices

CQRS

Swagger

Testing

And a lot more

Why use nest?

Same language frontend and backend

Fast and scalable

Application structure

Asynchronous model

High performance

Separation of concerns

@reibo_bo

http://slides.com/bovandersteene/nestjs

nestjs

By Bo Vandersteene