NestJS

Formation de 3 jours sur le Framework Node.js

Programme

L'écosystème Node.js

 

1.

2.

Nest.JS Basic

- Structure
- Controller
- Service
- Tests

3.

Nest.JS Ecosystème

- Communication aves une base de donnée
- Validation de donnée
- Guard

# CHAPTER 2

TPs

# CHAPTER 2

- Fil rouge
- Corrections en fin de Formation

  • 9h - 12h30
  • 14h00 - 17h30
     

Demandez-moi des pauses :)

Organisation

Florent Berthelot

- 8 ans dans 2 sociétés de services (Viseo, Zenika)
- Freelance (WeFacto)

- Actuellement en mission chez Hero (paiement B2B)

Node.Js

# Node.js

J'suis un developpeur C qui a problèmes asynchrones des

 

-Ryan Dahl-

# Node.js

+ API Système

# Node.js

Pourquoi ça a fonctionné ?

 

- APIs non bloquantes

- Single-thread

- KISS

- Pas de nouveau langage à apprendre !

 

# Node.js

Des APIs système :

Outillage Node.Js

Exercice 1

 

- Installez NVM et node

- Dans le terminal et écrivez successivement

node

console.log("hello world")

.help

.exit

C'était le REPL

Outillage Node.Js

Exercice 2

- Créez un dossier (nom de votre choix)

- Avec un terminal allez dans ce dossier

- Exécutez la commande `npm init`

- Répondez au questions

- Ajoutez `jest` comme dépendance de développement de ce projet

- Ajoutez un fichier .nvmrc
- Créez un fichier src/index.js qui affiche un Hello World

- Créez un script `npm start` qui lance le fichier src/index.js

Rappel TypeScript

Exercice 2bis

- Renommez le fichier src/index.js en src/index.ts

- Ajoutez typescript au projet via la commande

     npx -p typescript tsc --init

- Ajoutez un NPM Script "build" qui compile les fichiers TS dans un dossier build/
- Observez la différence entre le fichier JS et TS

Les tests

Exercice 3

Implémentez le code et les tests correspondant à ces spécifications


Scénario 1 :
Quand je rentre un nom et un mot de passe valide
Alors je suis authentifié


Scénario 2 :
Quand je rentre un nom qui n'existe pas
Alors je ne suis pas authentifié

Scénario 3 :

Quand je rentre un nom qui existe mais un mauvais mot de passe
Alors je ne suis pas authentifié

Les Modules

API system 1

Interactions avec les fichiers

Avant Propos

# FS: Avant propos

Comment gérer l'asynchrone en JavaScript ?

Avant Propos

# FS: Avant propos

Les callbacks !

const isPS5Available = (company, cb) => {
	setTimeout(() => {
    	if(company === 'M. Bricolage') {
        	cb(new Error('Wrong company'));
          	return;
        }
    	cb(null, false)
    }, 60_000);
}

Live coding !

FS, old style

# FS
const fs = require('fs');

fs.readFile('/etc/passwd', 'utf8', (err, data) => {
  if (err) {
    console.error(err);
  }
  console.log(data)
});

FS, transition way

# FS
const fs = require('fs');
const {promisify} = require('util');
const readFile = promisify(fs.readFile);

readFile('/etc/passwd', 'utf8')
	.then((data) => {
  		console.log(data)
	})
	.catch(err => {
		console.error(err);
	});

FS coolest way

# FS
import { readFile } from 'node:fs/promises';


readFile('/etc/passwd', 'utf8')
	.then((data) => {
  		console.log(data)
	})
	.catch(err => {
		console.error(err);
	});

API Synchrone ??

# FS
import { readFileSync } from 'node:fs';

readFileSync('/etc/password', 'utf8');

Attention, ça bloque absolument TOUT !

FS, comment lire la doc ?

# FS

Une histoire de chemin

# FS
// Pour avoir un chemin relatif au fichier dans lequel on code
import {join} from 'node:path';

const filePath = join(__dirname, './asset/users.csv')

Les mocks ?!

 

Mocker FS, un exemple

# Mock FS
import {readFile} from 'node:fs'

jest.mock('node:fs', () => {
  return {
    readFile: jest.fn()
  }
});


it('should work', () => (
  (readFile as jest.Mock)
  	.mockImplementation(() => 'user,password\nflorent,testMDP');
 
  // ...
));

Récupérer les arguments

Récupération d'arguments

# Argv
import { argv } from 'node:process';

argv.forEach((arg) => {
  console.log(arg);
});
npm run start -- hello world toto yolo 42
hello
world
toto
yolo
42

Exercice 4

Créez un fichier CSV avec ce format :

 

 

Maintenant, il faut lire ce fichier pour l'authentification.

 

Pour vérifier manuellement que cela fonctionne:




Pensez aux types de node avec :

user,password
florent,formation
npm start -- florent formation

# Cela doit retourner true
npm i -D @types/node

Écouter le réseau

Créer un server

# Server
const http = require('node:http');

// Create a local server to receive data from
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    data: 'Hello World!'
  }));
});

server.listen(8000);

Créer un server (TS)

# Server
import {createServer, IncomingMessage, ServerResponse} from 'node:http';

// Create a local server to receive data from
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    data: 'Hello World!'
  }));
});

server.listen(8000);

Objet Request

# Server

Objet Response

# Server

Faire des appels Résaux

C'est pas si simple 🙃

const https = require('https');

https.get('https://pokeapi.com', (resp) => {
  let data = '';

  // A chaque paquets reçu
  resp.on('data', (chunk) => {
    data += chunk;
  });

  // Une fois que toute la réponse est arrivé, on peut logguer
  resp.on('end', () => {
    console.log(JSON.parse(data).explanation);
  });

}).on("error", (err) => {
  console.log("Error: " + err.message);
});
# Appel HTTP

Exercice 5

Lorsque l'on exécute                             , alors c'est un serveur qui est lancé.


Lors que l'on fait un GET sur /pokemons alors :
- On récupère, via une API tierce (
pokeapi.co) la liste des Pokémons

- Il faut adapter chacun des Pokémons pour ne retourner que les informations nécessaire

 

npm run serve

L'écosystème Node.js

Les frameworks Node.js

# écosystème

- Express

- Koa

- Fastify

- Restify

- x Hapi

- Nest.js

- ...

 

Qui se cache derrière Nest.js

# écosystème

Kamil Mysliwiec


...


L'open-source :)
 

 

Nest.js ?

# écosystème

- Extensible : une architecture modulaire similaire à ce qui existe du côté front avec Angular.
 

- Versatile : l'écosystème est vaste et s'adaptera à faire ce que vous souhaitez (API GraphQL, API REST, ...).

 

- Progressif : l'architecture modulaire nous permet de n'utiliser que ce dont nous avons besoin.

Popularité

# écosystème

Popularité

# écosystème

Popularité

# écosystème

Nest.Js, un framework progressif

# écosystème

Nest.js
Outillage

Nest.js CLI

# Nest.js outillage
$ npm i -g @nestjs/cli

$ nest new project-name

Nest.js CLI

# Nest.js outillage

Nest.js
Outillage

Exercice 6

La doc nous dit :

 

 

Utilisons npx !

 

Créer votre projet Nest.js et démarrez-le.
Pensez à regarder le package.json.
Regardez l'outillage mis en place.

Faire en sorte que la route GET / retourne votre nom.

npm i -g @nestjs/cli

nest new project-name

Nest.js
Les tests

Test avec supertest

// Test E2E

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });
});
# Test Nest.js

Les test plus "unitaire" seront vu plus tard :-)

# Test Nest.js

Exercice 7

Réparez les test "End-To-End"

Nest.js

Controllers

Controller

# Controller

Notre controller

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private appService: AppService) {}

  @Get()
  getHello(): string {
    return 'Florent Berthelot';
  }
}



// GET /
# Controller

L'argument du controller

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('users')
export class UserController {
  constructor(private appService: AppService) {}

  @Get()
  getUsers(): string[] {
    return ['Florent Berthelot', 'Remi'];
  }
}


// GET /users
# Controller

Request

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('users')
export class UserController {
  constructor(private appService: AppService) {}

  @Get()
  getUsers(@Req() request: Request): string[] {
    return ['Florent Berthelot', 'Remi'];
  }
}


// GET /users
# Controller

Les params

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('users')
export class UserController {
  constructor(private appService: AppService) {}

  @Get()
  getUsers(@Req() request: Request): string[] {
    return ['Florent Berthelot', 'Remi'];
  }
  
  @Get(':id')
  getUser(@Param('id') id: string): string {
    return id === '1' ? 'Remi' : 'Florent Berthelot';
  }
}


// GET /users
// GET /users/:id (ex: /users/2)
# Controller

L'asynchrone

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import {of} from 'rxjs';

@Controller('users')
export class UserController {
  constructor(private appService: AppService) {}

  @Get()
  getUsers(@Req() request: Request): Promise<string[]> {
    return Promise.resolve(['Florent Berthelot', 'Remi']);
  }
  
  @Get(':id')
  getUser(@Param('id') id: string): Observable<string> {
    return of(id === '1' ? 'Remi' : 'Florent Berthelot');
  }
}


// GET /users
// GET /users/:id (ex: /users/2)
# Controller

Déclarer le controller

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';

@Module({
  imports: [],
  controllers: [UserController],
  providers: [],
})
export class AppModule {}
# Controller

Créer un controller

nest generate controller user

nest g co user
# Controller

Nest.js

Controllers

BIS
Resolvers

Installation de graphql

npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express
# Graphql

Installation de graphql

npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express
# Graphql
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
    }),
  ],
})
export class AppModule {}

http://localhost:3000/graphql

Les types graphql

# Graphql
import { Field, Int, ObjectType } from '@nestjs/graphql';


@ObjectType()
export class User {
  @Field(type => Int)
  id: number;

  @Field()
  name: string;

  @Field({ nullable: true })
  age?: number;
}

Les resolvers graphql

# Graphql
import {User} from './user.model'


@Resolver(of => User)
export class UserResolver {

  @Query(returns => User)
  async user() {
    return {
      id: 1,
      name: 'toto',
      age: 42.5
    };
  }
}

Les resolvers graphql

# Graphql
import {User} from './user.model'


@Resolver(of => User)
export class UserResolver {

  @Query(returns => User)
  async users(@Args('id', { type: () => Int }) id: number) {
    if(id !== 1) {
      throw new Error('NOT_FOUND')
    }
    return {
      id: 1,
      name: 'toto',
      age: 42.5
    };
  }
}

Les resolvers graphql

# Graphql
import {User} from './user.model'
import {getFriend} from './friendFinder'

@Resolver(of => User)
export class UserResolver {

  @Query(returns => User)
  async users(@Args('id', { type: () => Int }) id: number) {
    if(id !== 1) {
      throw new Error('NOT_FOUND')
    }
    return {
      id: 1,
      name: 'toto',
      age: 42.5
    };
  }
  
  @ResolveField()
  async friends(@Parent() user: User) {
    const { id } = user;
    return getFriend(id);
  }
}

Les resolvers graphql

# Graphql
import {User} from './user.model'


@Resolver(of => User)
export class UserResolver {

  @Query(returns => User)
  async user(@Args('id', { type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveField()
  async posts(@Parent() author: Author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }
}

Exercice 8

Codez et testez les routes suivantes :

 

- GET /pokemon

- GET /pokemon/:id

Exercice 8 bis

Codez et testez le schéma GraphQL suivant :

type Pokemon {
  # ... #
}

query {
  pokemons: Pokemon[]
  pokemon(id: String!): Pokemon!
}

mutation {
  login(username: String!, password: String!)
}

Nest.js

DTO

Les query params

import { Controller, Get, Body, Query } from '@nestjs/common';
import { AppService } from './app.service';
import { UserDTO } from './user.dto';

let users: UserDTO[] = [{name: 'Florent Berthelot'}, {name: 'Remi'}];

@Controller('users')
export class AppController {
  constructor(private appService: AppService) {}

  @Get()
  getUsers(@Query("search") search: string): UserDTO[] {
    return users.filter(/*...*/);
  }
}

// GET /users?search (ex /users?search=toto)
# DTO

Le body

import { Controller, Get, Body } from '@nestjs/common';
import { AppService } from './app.service';
import { UserDTO } from './user.dto';

let users: UserDTO[] = [{name: 'Florent Berthelot'}, {name: 'Remi'}];

@Controller('users')
export class AppController {
  constructor(private appService: AppService) {}

  @Get()
  getUsers(): UserDTO[] {
    return users;
  }
  
  @Put()
  replaceUsers(@Body() newUsers: UserDTO[]):  UserDTO[] {
   	users = newUsers;
  	return users;
  }
}

// GET /users
// PUT /users {name: "toto"}
# DTO

Le body

export class UserDTO {
  name: string
}
# DTO

Tester un controller !

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Florent Berthelot');
    });
  });
});
# DTO

Nest.js

DTO Bis

Les mutations

Les mutations, les arguments

import { InputType, Field } from '@nestjs/graphql';

@InputType()
export class UserInput {
  @Field()
  name: string;
  
  @Field()
  age: number;
}
# Graphql

Les mutations

# Graphql
import {User} from './user.model'
import {UserInput} from './UserInput.DTO'

@Resolver(of => User)
export class UserResolver {

  @Mutation(returns => User)
  async addUser(@Args('userInput') user: UserInput) {
    return addUserToDatabase(user);
  }
}

Exercice 9

Codez et testez les routes suivantes :


- POST /login

- GET /Pokemon

- GET /Pokemon/:id

- POST /Pokemon

- DELETE /Pokemon/:id

Exercice 9 bis

Codez et testez le schéma GraphQL suivant :

type Pokemon {
  # ... #
}

query {
  pokemons: Pokemon[]
  pokemon(id: String!): Pokemon!
}

mutation {
  login(username: String!, password: String!)
  addPokemon() 
}

Nest.js

Les services

Notre service

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Florent Berthelot';
  }
}
# Services

Ce sont des singletons !

On peut stocker des données !

L'injection ?

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
# Services

La déclaration au module

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
# Services

La déclaration au module

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [{
    provide: AppService,
    useValue: {
      getHello: () => 'Yoooooo'
    }
  }],
})
export class AppModule {}
# Services

La déclaration au module

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [{
    provide: AppService,
    useValue: {
      getHello: () => 'Yoooooo'
    }
  }],
})
export class AppModule {}
# Services

Une hiérarchie d'injection à la Angular

# Services

Créer un Service

nest generate service auth

nest g s auth
# Services

Tester un Service

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [
        {
          provide: AppService,
          useValue: {
            getHello: () => 'Yooooo',
          },
        },
      ],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});
# Services

Exercice 10

Déplacez votre logique métier dans des services

Nest.js

Les appels HTTP

Installer le module HTTP

$ npm i --save @nestjs/axios
# Appel HTTP
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HttpModule } from '@nestjs/axios'

@Module({
  imports: [HttpModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Installer le module HTTP

# Appel HTTP
@Injectable()
export class UserService {
  constructor(private httpService: HttpService) {}

  findAll(): Observable<AxiosResponse<User[]>> {
    return this.httpService.get('http://localhost:8000/user');
  }
}

Installer le module HTTP

# Appel HTTP
@Injectable()
export class UserService {
  constructor(private httpService: HttpService) {}

  findAll(): Observable<AxiosResponse<User[]>> {
    return this.httpService.get('http://localhost:8000/user');
  }
}
@Injectable()
export class UserService {
  constructor(private httpService: HttpService) {}

  findAll(): Promise<AxiosResponse<User[]>> {
    return this.httpService.get('http://localhost:8000/user').toPromise();
  }
}

Exercice 11

Maintenant, les statistiques de vos Pokémon son issue de la PokéAPI.

Nest.js

Les modules

Une architecture modulaire

# Modules

Une architecture modulaire

# Modules
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  imports: [],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}
import { Module } from '@nestjs/common';
import { UserModule } from './user/user.controller';

@Module({
  imports: [UserModule],
  controllers: [],
  providers: [],
})
export class AppModule {}

Les modules partagé

# Modules

Modules partagés

# Modules
@Module({
  imports: [CommonModule],
  controllers: [],
  providers: [],
  exports: [HTTPService, CommonModule]
})
export class HTTPModule {}

Le démarrage !

# Modules
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Créer un module

nest generate module user

nest g mo user
# Module

Exercice 12

Séparez la gestion de l'authentification et celle des Pokemons dans 2 modules à part.

Nest.js

Les Pipes

Pipe, middleware

# Les pipes

Pipe, parseInt

# Les pipes
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('users')
export class UserController {
  constructor(private appService: AppService) {}
  @Get(':id')
  getUser(@Param('id', ParseIntPipe) id: number): string {
    return id === 1 ? 'Remi' : 'Florent Berthelot';
  }
}


// GET /users/:id (ex: /users/2)

Pipe, parseInt

# Les pipes
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('users')
export class UserController {
  constructor(private appService: AppService) {}
  @Get(':id')
  getUser(@Param('id', ParseIntPipe) id: number): string {
    return id === 1 ? 'Remi' : 'Florent Berthelot';
  }
}


// GET /users/:id (ex: /users/2)

Si id n'est pas un nombre entier, alors on renvoi une erreur 400

Pipe utilisation multiple

# Les pipes
@Get()
async findAll(
  @Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
  @Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
  return this.catsService.findAll({ activeOnly, page });
}

Pipes fourni

# Les pipes

Pipe création

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

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if(value !== 'valid') {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
}

Créer un Pipe

nest generate pipe validator

nest g pi user
# Pipes

Nest.js

Validation

Validation Pipe

# Les pipes
$ npm i --save class-validator class-transformer
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

Validation DTO !

# Les pipes
export class UserDTO {
  @IsString()
  @MinLength(5)
  @MaxLength(200)
  name: string
}

Class Validator

# Les pipes

Exercice 13

Ajouter une étape de validation des données lors :
  - du login

  - de l'ajout d'un pokémon

Nest.js

Documentation

Open-API (ex-Swagger) !

npm install --save @nestjs/swagger
# Documentation
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('Cats example')
    .setDescription('The cats API description')
    .setVersion('1.0')
    .addTag('cats')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

Open-API (ex-Swagger) !

# Documentation

Open-API (ex-Swagger) !

# Documentation
import { ApiProperty } from '@nestjs/swagger';
import {IsString, IsInt, Min} from 'class-validator';

export class CreateUserDto {
  @ApiProperty()
  @IsString()
  name: string;

  @ApiProperty()
  @IsInt()
  @Min(0)
  age: number;
}

Open-API (ex-Swagger) !

# Documentation
import { ApiProperty } from '@nestjs/swagger';
import {IsString, IsInt, Min} from 'class-validator';

export class CreateUserDto {
  @ApiProperty()
  @IsString()
  name: string;

  @ApiProperty({
    description: 'The age of the user',
    minimum: 0,
    default: 18
  })
  @IsInt()
  @Min(0)
  age: number;
}

Open-API (ex-Swagger) !

# Documentation
import { ApiProperty } from '@nestjs/swagger';
import {IsString, IsInt, Min} from 'class-validator';

export class CreateUserDto {
  /**/
  @ApiProperty({
    type: [String]
  })
  friend: string[]
  
  @ApiProperty({ enum: ['Admin', 'Moderator', 'User']})
  role: 'Admin' | 'Moderator' | 'User'
}

Open-API (ex-Swagger) !

# Documentation
import { Body, Controller, Get, Param } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
import { AppService } from './app.service';

@ApiTags('Pokemon')
@Controller('pokemon')
export class AppController {
  constructor(private appService: AppService) {}

  @ApiResponse({ status: 200, description: 'kan tout va bi1' })
  @ApiResponse({ status: 400, description: "kan t'a mal fait" })
  @ApiParam({ name: 'id', description: 'id du user' })
  @Post()
  getHello(@Param('id') id: string, @Body() poke): string {
    console.log(id);
    return this.appService.getHello();
  }
}

Documentation-first ?!

# Documentation

- Définir les routes que l'on expose (en équipe)

- Définir les DTOs (en équipe)

- Implémenter, tester ces routes (en solo/pair)
 

Exercice 14

Modifiez vos DTOs et vos controllers pour que votre documentation soit le reflet de votre API.

Nest.js

Les erreurs

Des filtres ?

# Les erreurs 

Throw It !

// Controller

import {HttpException, HttpStatus} from  '@nestjs/common';

/** ... **/

@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
# Les erreurs 

Throw It !

// Controller

import {HttpException, HttpStatus} from  '@nestjs/common';

/** ... **/

@Get()
async findAll() {
   throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, HttpStatus.FORBIDDEN);
}
# Les erreurs 

Throw It !

# Les erreurs 

Filter errors

# Les erreurs 
// http-exception.filter.ts

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    
    console.error('An error occured, oopsy')

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

Filter errors

# Les erreurs 
// Un controller

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

Filter errors

# Les erreurs 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

Filter errors

# Les erreurs 
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

Créer un filtre

nest generate filter HTTPexeptionLogger

nest g f HTTPexeptionLogger
# Pipes

Nest.js

Les intercepteurs

Les intercepteurs

# Intercepteurs

Les intercepteurs

# Intercepteurs
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  RequestTimeoutException
} from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000),
      catchError(err => {
        if (err instanceof TimeoutError) {
          return throwError(() => new RequestTimeoutException());
        }
        return throwError(() => err);
      }),
    );
  };
};

Les intercepteurs

# Intercepteurs
@UseInterceptors(TimeoutInterceptor)
export class UsersController {
  
  /** **/
}
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TimeoutInterceptor());

Créer un intercepteur

nest generate interceptor <interceptorName>

nest g in <interceptorName>
# Pipes

Exercice 15

Créez un intercepteur qui LOG chacune des requêtes arrivant sur le server.

Nest.js

TypeORM

# TypeORM

Object-Relation Mapping

Kézako ??

Une requête SQL ?

Création de la requête

1.

2.

Éxecution

 

3.

Adaptation du resultat

Dans un objet représentatif de la ce qu'il y a en Base de Donnée.

# TypeORM
# CHAPTER 2

Active Record

# CHAPTER 2

Data Mapper

Une entité DB

# TypeORM
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class UserDB {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    firstName: string

    @Column()
    lastName: string

    @Column()
    age: number
}

Jointures DB

# TypeORM
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity('pet')
export class PetsDB {
    @PrimaryGeneratedColumn()
    id: number
  	
    @ManyToOne(() => UserDB, (user) => user.pets, {
        eager: true,
    })
    user: UserDB
}




@Entity('user')
export class UserDB {
    @PrimaryGeneratedColumn()
    id: number

    @OneToMany(() => PetsDB, (pet) => pet.user)
    pets: PetsDB[]
  	
}

Active Record

# TypeORM
const user = new User()
user.firstName = "Timber"
user.lastName = "Saw"
user.isActive = true
await user.save()

Data Mapper

# TypeORM
const userRepository = dataSource.getRepository(User)


const user = new User()
user.firstName = "Timber"
user.lastName = "Saw"
user.isActive = true
await userRepository.save(user)

Avec Nest.js ?

npm install --save @nestjs/typeorm typeorm mysql2


docker run --name training-nestjs -e MYSQL_ROOT_PASSWORD=root -d mysql:latest
# TypeORM

Avec Nest.js ?

# TypeORM
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './user/user.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [UserEntity],
      synchronize: false, // important, mauvaise pratique
    }),
  ],
})
export class AppModule {}

Repository

# TypeORM
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: string): Promise<User> {
    return this.usersRepository.findOne(id);
  }

  async remove(id: string): Promise<void> {
    await this.usersRepository.delete(id);
  }
}

Transaction

# TypeORM
import {UserDB} from './user.entity';
import {User} from './user.model';

@Injectable()
export class UsersService {
  constructor(private dataSource: DataSource) {}
  
  async createUser(user: User[]) {
  	const queryRunner = this.dataSource.createQueryRunner();

  	await queryRunner.connect();
  	await queryRunner.startTransaction();
    
    try {
      await queryRunner.manager.save(
      	new UserDB(...user)
      );

      await queryRunner.commitTransaction();
    } catch (err) {
      await queryRunner.rollbackTransaction();
    }
    
    await queryRunner.release();
  }
}

Exercice 16

Les Pokemons que vous ajoutez lors du Post /pokemons sont maintenant stocké en base de donnée.

Nest.js

Autorisation

&

Authentification

# CHAPTER 2

Identification

Action consistant à identifier un objet ou un individu.

Exemple : "Je suis le président"

Processus permettant au système de s’assurer de la légitimité de la demande d’accès faite.

Exemple : "Ok, c'est bien le président, il est bien celui qui dit qu'il est !"

Authentification

401 Unauthorized
# CHAPTER 2

Authorisation

Fonction spécifiant les droits d’accès vers les ressources.


Exemple : "Oui, l'admin a le droit de lancer la bombe atomique"

403 Forbidden

JWT

# Authorisation

JWT

# Authorisation
npm install jsonwebtoken
import jwt from 'jsonwebtoken';

const token = jwt.sign(
  {
    role: ['admin']
  },
  'secret-tres-important',
  { expiresIn: '1h' }
);

Forger un Token

JWT

# Authorisation
import jwt from 'jsonwebtoken';

try {
  const decoded = jwt.verify(token, 'secret-tres-important');
  console.log(decoded)
} catch(err) {
  console.error(err);
}

Vérifier un Token

Et Nest.js dans tout ça ?

# Authorisation
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

Et Nest.js dans tout ça ?

# Authorisation
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const request = context.switchToHttp().getRequest<Request>();
    const token = request.headers.get('Authorization')?.replace('Bearer ', '');

    try {
      jwt.verify(token, 'secret-tres-important');
      return true;
    } catch(err) {
      return false;
    }
  }
}

Utilisation d'un guard

# Authorisation
@Controller('pokemon')
@UseGuards(AuthGuard)
export class PokemonController {

/** ... **/
}
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new AuthGuard());

Ajout de métadonnées

# Authorisation
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createPokemonDto: CreatePokemonDto) {
  this.catsService.create(createPokemonDto);
}

Utilisation de métadonnées

# Authorisation
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    /** **/

	const roles = this.reflector.get<string[]>('roles', context.getHandler());
  }
}

Exercice 17

La route POST /login renvoi un JWT.

L'accès à la route POST /pokemon n'est autorisé que pour ceux qui sont loggué.

Astuce de fin

nest generate ressource pokemon2

Florent Berthelot

https://berthelot.io

Made with Slides.com