CRÉONS NOTRE PREMIÈRE API AVEC NESTJS

Le framework Node.js à la façon Angular

Emmanuel DEMEY

Freelance

@EmmanuelDemey

emmanuel

Aurélien LOYER

Software Engineer @ Qima

@AurelienLoyer

aurelien

Petite Présentation

  • 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

Installation

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

Par où commencer ?

main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

Par où commencer ?

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

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

Présentation du TP fil rouge

Intégration Swagger

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);
}

Intégration Swagger

  • 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";
  }

}

Intégration Swagger

import { ApiProperty } from '@nestjs/swagger';

class Address {
  @ApiProperty()
  city: string
}

class User {
  @ApiProperty()
  name: string;

  @ApiProperty({type: Address})
  address: Address;
}

À vous de jouer !

  • 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

Les contrôleurs

Les contrôleurs

  • 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';
  }
}

Les contrôleurs

  • 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 {};
  }
}

Les contrôleurs

  • 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([]);
    }
}

Les contrôleurs - autres décorateurs

@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) { }
}

À vous de jouer !

  • 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

Les Modules

Les Modules

  • 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

    • …​

Les Modules

  • 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 {}

Les Modules - @Module

  • 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 {}

Les Modules - Module dynamique

  • 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])

À vous de jouer !

  • 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

Les Providers

Les Providers

  • 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

Les Providers

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 {}

Les Providers

  • 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();
  }
}

À vous de jouer !

  • Mettre tout le code métier utilisé par la classe ProductsController dans un service dédié ProductsService

Validation des données

Validation des données

  • 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) { ... }

Validation des données

  • 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) { ... }

Validation des données

  • Le validateur se base sur les modules class-validator et class-transformer

npm install -D class-validator class-transformer

Validation des données

  • 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;
}

Custom validateur

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;
 */

À vous de jouer !

  • 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 `

Tests

Tests

  • 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)

Tests

  • Module (installé par défaut) proposant des utilitaires pour écrire des tests

npm i -D @nestjs/testing

Tests

  • Plusieurs commandes à connaitre

npm run test
npm run test:watch
npm run test:cov
npm run test:e2e

Tests Unitaires

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);
    });
  });
});

Tests Unitaires

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);
    });
  });
});

Tests End2End

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();
  });
});

À vous de jouer !

  • Ecrivez des tests (unitaires et/ou end2end) pour l’application que nous sommes en train de développer.

Authentication

Authentication

  • 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

Authentication - JWT

  • 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 [];
}

Authentication - JWT

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 {}

Authentication - JWT

@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;
  }
}

Authentication - JWT

@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);
  }
}

À vous de jouer !

  • 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

Pour aller plus loin…​

Un zeste de Nest…​

conference

SSR pour application Angular

Merci 🙏

@EmmanuelDemey | @AurelienLoyer