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
NestJS
By Florent Berthelot
NestJS
- 447