Emmanuel DEMEY
Freelance
@EmmanuelDemey
Le framework Node.js à la façon Angular
Emmanuel DEMEY
Freelance
@EmmanuelDemey
Aurélien LOYER
Software Engineer @ Qima
@AurelienLoyer
Plateforme permettant de développer des applications serveur basées sur :
Node.js
TypeScript
Express ou Fastify
Ouvert à GraphQL, WebSockets, monde des microservices (Redis, gRPC, MQTT, …)
Fortement inspiré de l’architecture d’une application Angular
Même structure
Même terminonologie
Mêmes patterns
Utilisation d’une CLI pour bootstraper le projet
Même philosophie que pour Angular CLI
npm i -g @nestjs/cli
nest new project-name
npm run start:dev
npm run start:debug
npm run test
Propose des schematics pour générer de nouveaux fichiers
nest generate controller users
nest generate provider users
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
npm install @nestjs/swagger swagger-ui-express
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const options = new DocumentBuilder()
.setTitle('Users example')
.setDescription('The users API description')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
Documentation de vos APIs via des Décorateurs
import { Controller, Post, Body } from '@nestjs/common';
import { ApiResponse } from '@nestjs/swagger';
@Controller()
export class AppController {
@Post()
@ApiResponse({ status: 403, description: 'Forbidden.'})
createUser(@Body() user: User): string {
return "OK";
}
}
import { ApiProperty } from '@nestjs/swagger';
class Address {
@ApiProperty()
city: string
}
class User {
@ApiProperty()
name: string;
@ApiProperty({type: Address})
address: Address;
}
Récupérez le répertoire step0
du repository GIT
git clone -b step0 https://github.com/T3kstiil3/codelab-nestjs-corrections/
cd codelab-nestjs-corrections
npm install
Ajoutez l’intégration Swagger à l’application NestJS
Testez votre application pour vérifier son bon fonctionnement
Un contrôleur permet de définir les endpoints de votre API REST.
Syntaxe pour définir les headers, les path parameters, les query parameters, … facilitée par l’utilisation des décorateurs
import { Controller, Get } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
findAll(): string {
// GET /users
return 'All NestJS users';
}
}
Utilisation des décorateurs @Get, @Post, @Put, @Delete, … pour définir le verbe HTTP à utiliser.
@Controller('uers')
export class UsersController {
@Get()
findAll(): User[] {
return [];
}
@Get(':id')
findById(@Param() id: string): User {
return {};
}
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: User) : User {
return {};
}
}
Retourner la donnée directement
Retourner une Promise
Manipuler l’objet Response
import { Response } from 'express';
@Controller('users')
export class UsersController {
@Get()
findAll(): User[] {
return [];
}
@Get()
findAllFromRemoteServer(): Promise<User[]> {
return fetchUserFromAnotherServer();
}
@Get()
findAllResponse(@Res() res: Response) {
res.status(HttpStatus.OK).send([]);
}
}
@Controller('users')
export class UsersController {
@Get()
findAll(
@Query('sort') sort:string,
@Headers('pageNumber') pageNumber: number
): User[] { ... }
@Put(':id')
update(
@Param('id') id: string,
@Body() updateUserDto: User) { }
}
Dans le AppController
, implementez un CRUD permettant de gérer des produits (en mémoire). Nous allons pouvoir :
Lister des produits
Récupérer une produit
Supprimer une produit
Ajouter une produit
Modifier une produit
Toute application NestJS possède au moins un module
A ne pas confondre aves les modules ES2015
Permet de regrouper de manière logique un ensemble de fonctionnalités
découpage métier
découpage technique
…
Nécessité ensuite d’importer ces modules pour pouvoir les utiliser
import { Module } from '@nestjs/common';
import { HomeController } from './home.controller';
import { UserController } from './users';
@Module({
controllers: [UserController]
})
export class UserModule {}
@Module({
controllers: [HomeController],
imports: [UserModule],
})
export class AppModule {}
Seules 4 propriétés sont à connaître : controllers
, providers
, imports
, exports
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { DatabaseModule } from './database.module';
import { AuthProvider } from './auth.service';
import { UserProvider } from './user.service';
@Module({
controllers: [AuthController],
imports: [DatabaseModule],
providers: [AuthProvider, UserProvider],
exports: [AuthProvider]
})
export class AuthModule {}
Tout comme Angular, possibilité de définir des modules dynamiques afin de les configurer
@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
//DatabaseModule.forRoot([User])
Via la CLI, générez un nouveau module NestJS ProductsModule
.
Dans ce module générez un nouveau contrôleur ProductsController
qui contiendra le CRUD créé précédemment
Importez ce nouveau module dans le module principal afin d’avoir le même fonctionnement que précédemment
Type de classes (provider, factory, service, …) permettant d’extraire le code business de votre application
Système d’Injections de Dépendances activé par défaut
Singletons configurés via la classe annotée @Module
import { Injectable } from '@nestjs/common';
import { User } from './interfaces/user.interface';
@Injectable()
export class UsersService {
private readonly users: User[] = [];
findAll(): User[] {
return this.users;
}
}
import { Module } from '@nestjs/common';
import { UsersController } from './users/users.controller';
import { UsersService } from './users/users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class ApplicationModule {}
Injections de ces singletons via les paramètres du constructeur
import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './interfaces/user.interface';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll(): Promise<User[]> {
return this.usersService.findAll();
}
}
Mettre tout le code métier utilisé par la classe ProductsController
dans un service dédié ProductsService
Grâce au pipe ValidationPipe
, nous allons pouvoir valider les données entrantes
La validation peut s’activer
localement à un endpoint
@Post()
@UsePipes(ValidationPipe)
getHello(@Body() body: User) { ... }
globalement pour toute l’application
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
localement à un paramètre
@Post()
getHello(@Body(new ValidationPipe()) body: User) { ... }
Le validateur se base sur les modules class-validator
et class-transformer
npm install -D class-validator class-transformer
Il suffit d’ajouter des décorateurs sur vos models
import { IsString, IsInt, IsEmail, IsDate } from 'class-validator';
export class User {
@IsString()
readonly name: string;
@IsInt()
readonly age: number;
@IsEmail()
readonly email: string;
@IsDate()
readonly birthDate: date;
}
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from "class-validator";
@ValidatorConstraint({
name: "passwordValidator",
async: false
})
export class PasswordValidator implements ValidatorConstraintInterface {
validate(password: string, args: ValidationArguments) {
return password.length > 5;
}
defaultMessage(args: ValidationArguments) {
return "Erreur: 'password' doit être plus compliqué 😨";
}
}
/**
@Validate(PasswordValidator)
readonly password: string;
*/
Activez la validation de manière globale à l’application
Ajouter les contraintes suivantes sur votre classe Beer
id
doit être un number
label
doit être une string
description
doit être une string
, avec un taille entre 10 et 80 caractères
image
doit être une string
, et se terminer par un extension valide (via un custom validateur)
price
doit être une number
, compris entre 0 et 100
stock
doit être une entier
`
NestJS propose une intégraton pour écrire
des tests unitaires (via Jest)
des tests end2end (via Supertest)
Ces deux solutions nécessitent de définir la partie de l’application testée
Définition d’un TesBed
Possibilité de surcharger certaines briques (accés la base de données ou à une API tierce)
Module (installé par défaut) proposant des utilitaires pour écrire des tests
npm i -D @nestjs/testing
Plusieurs commandes à connaitre
npm run test
npm run test:watch
npm run test:cov
npm run test:e2e
describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;
beforeEach(() => {
catsService = new CatsService();
catsController = new CatsController(catsService);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = module.get<CatsService>(CatsService);
catsController = module.get<CatsController>(CatsController);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
describe('Cats', () => {
let app: INestApplication;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [CatsModule],
});
app = module.createNestApplication();
await app.init();
});
it(`/GET cats`, () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect({
data: [...]
});
});
afterAll(async () => {
await app.close();
});
});
Ecrivez des tests (unitaires et/ou end2end) pour l’application que nous sommes en train de développer.
NestJS se base sur la librairie Passport pour pouvoir restreindre l’accès à nos endpoints
Nous allons pouvoir définir des Strategy
d’authentification et les utiliser via des Guards
Modules de base nécésaires pour la mise en place de l’authentification
npm install --save @nestjs/passport passport
En fonction de la Strategy
, d’autres modules seront nécéssaires
npm install --save @nestjs/jwt passport-jw
@Get('users')
@UseGuards(AuthGuard())
findAll() {
return [];
}
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '@nestjs/passport';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secretOrPrivateKey: 'secretKey'
})
],
providers: [AuthService, JwtStrategy]
})
export class AppModule {}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'secretKey',
});
}
async validate(payload: JwtPayload) {
const user = await this.authService.validateUser(payload);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async signIn(): Promise<string> {
const user: JwtPayload = { email: 'user@email.com' };
return this.jwtService.sign(user);
}
async validateUser(payload: JwtPayload): Promise<User> {
return await this.usersService.findOneByEmail(payload.email);
}
}
Installez les modules NPM nécessaires pour une Authentification JWT
Via le service JwtService
, implémentez la méthode login
du service UserService
Elle doit retourner un objet { expiresIn: 3600, accessToken: `Bearer ${accessToken}
, }`, avec accessToken
le résultat retourné par la méthode sign
du service JwtService
Sécurisez la route permettant de créer et mettre à jour des produits
@EmmanuelDemey | @AurelienLoyer
@nestframework @EmmanuelDemey @AurelienLoyer