Applications TypeScript à pile complète

Bonjour, Habr! Je vous présente la traduction de l'article "Full Type Stack TypeScript Apps - Part 1: Developing Backend APIs with Nest.js" par Ana Ribeiro .


Partie 1: DĂ©veloppement de l'API serveur Ă  l'aide de Nest.JS


TL; DR: il s'agit d'une série d'articles sur la création d'une application Web TypeScript à l'aide d'Angular et de Nest.JS. Dans la première partie, nous écrirons une simple API serveur utilisant Nest.JS. La deuxième partie de cette série est consacrée à l'application frontale utilisant Angular. Vous pouvez trouver le code final développé dans cet article dans ce référentiel GitHub.


Qu'est-ce que Nest.Js et pourquoi Angular?


Nest.js est un cadre pour la création d'applications de serveur Web Node.js.


Une particularité est qu'il résout un problème qu'aucun autre framework ne résout: la structure du projet node.js. Si vous avez déjà développé sous node.js, vous savez que vous pouvez faire beaucoup avec un seul module (par exemple, le middleware Express peut tout faire, de l'authentification à la validation), ce qui peut finalement conduire à un "gâchis" non pris en charge . Comme vous le verrez ci-dessous, nest.js nous aidera à cela en fournissant des classes spécialisées dans divers problèmes.


Nest.js est fortement inspiré par Angular. Par exemple les deux plates-formes utilisent des gardes pour autoriser ou empêcher l'accès à certaines parties de vos applications, et les deux plates-formes fournissent une interface CanActivate pour implémenter ces gardes. Cependant, il est important de noter que, malgré certains concepts similaires, les deux structures sont indépendantes l'une de l'autre. Autrement dit, dans cet article, nous allons créer une API indépendante pour notre front-end, qui peut être utilisée avec n'importe quel autre framework (React, Vue.JS et ainsi de suite).


Application Web pour les commandes en ligne


Dans ce guide, nous allons créer une application simple dans laquelle les utilisateurs peuvent passer des commandes dans un restaurant. Il implémentera cette logique:


  • tout utilisateur peut afficher le menu;
  • seul un utilisateur autorisĂ© peut ajouter des marchandises au panier (passer une commande)
  • seul l'administrateur peut ajouter de nouveaux Ă©lĂ©ments de menu.

Par souci de simplicité, nous n'interagirons pas avec une base de données externe et n'implémenterons pas la fonctionnalité de notre panier.


Création de la structure de fichiers du projet Nest.js


Pour installer Nest.js, nous devons installer Node.js (v.8.9.x ou supérieur) et NPM. Téléchargez et installez Node.js pour votre système d'exploitation à partir du site Web officiel (NPM est inclus). Une fois tout installé, vérifiez les versions:


node -v # v12.11.1 npm -v # 6.11.3 

Il existe différentes façons de créer un projet avec Nest.js; ils se trouvent dans la documentation . Nous utiliserons nest-cli . Installez-le:


npm i -g @nestjs/cli


Ensuite, créez notre projet avec une simple commande:


nest new nest-restaurant-api


dans le processus, nest nous demandera de choisir un gestionnaire de paquets: npm ou yarn


Si tout s'est bien passé, nest créera la structure de fichiers suivante:


 nest-restaurant-api ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ └── main.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── .gitignore ├── .prettierrc ├── nest-cli.json ├── package.json ├── package-lock.json ├── README.md ├── tsconfig.build.json ├── tsconfig.json └── tslint.json 

allez dans le répertoire créé et démarrez le serveur de développement:


  #    cd nest-restaurant-api #   npm run start:dev 

Ouvrez un navigateur et entrez http://localhost:3000 . Sur l'Ă©cran, nous verrons:


Dans le cadre de ce didacticiel, nous ne testerons pas notre API (bien que vous deviez écrire des tests pour toute application prête à l'emploi). De cette façon, vous pouvez effacer le répertoire de test et supprimer le src/app.controller.spec.ts (qui est celui de test). Par conséquent, notre dossier source contient les fichiers suivants:


  • src/app.controller.ts et src/app.module.ts : ces fichiers sont responsables de la crĂ©ation Hello world message Hello world long de la route / . Parce que ce point d'entrĂ©e n'est pas important pour cette application nous les supprimons. BientĂ´t, vous apprendrez plus en dĂ©tail ce que sont les contrĂ´leurs et les services .
  • src/app.module.ts : contient une description d'une classe de type module , qui est chargĂ©e de dĂ©clarer l'importation, l'exportation des contrĂ´leurs et des fournisseurs vers l'application nest.js. Chaque application a au moins un module, mais vous pouvez crĂ©er plus d'un module pour des applications plus complexes (plus dans la documentation . Notre application ne contiendra qu'un seul module
  • src/main.ts : c'est le fichier responsable du dĂ©marrage du serveur.

Remarque: après avoir supprimé src/app.controller.ts et src/app.module.ts vous ne pourrez pas démarrer notre application. Ne vous inquiétez pas, nous allons le réparer bientôt

Créer des points d'entrée (points d'extrémité)



Notre API sera disponible sur la route /items . Ce point d'entrée permet aux utilisateurs de recevoir des données et aux administrateurs de gérer le menu. Créons-le.


Pour ce faire, créez un répertoire appelé items dans src . Tous les fichiers associés à la route /items seront stockés dans ce nouveau répertoire.


Création de contrôleurs


dans nest.js , comme dans de nombreux autres frameworks, les contrôleurs sont responsables du mappage des routes avec des fonctionnalités. Pour créer un contrôleur dans nest.js utilisez le décorateur nest.js comme suit: @Controller(${ENDPOINT}) . De plus, afin de mapper diverses méthodes HTTP , telles que GET et POST , des décorateurs @Get , @Post , @Delete , etc. sont utilisés.


Dans notre cas, nous devons créer un contrôleur qui retourne les plats disponibles dans le restaurant et que les administrateurs utiliseront pour gérer le contenu du menu. Créons un fichier appelé items.controller.tc dans le répertoire src/items avec le contenu suivant:


  import { Get, Post, Controller } from '@nestjs/common'; @Controller('items') export class ItemsController { @Get() async findAll(): Promise<string[]> { return ['Pizza', 'Coke']; } @Post() async create() { return 'Not yet implemented'; } } 

afin de rendre notre nouveau contrĂ´leur disponible dans notre application, enregistrez-le dans le module:


  import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; @Module({ imports: [], controllers: [ItemsController], providers: [], }) export class AppModule {} 

Lancez notre application: npm run start:dev et ouvrez dans le navigateur http: // localhost: 3000 / items , si vous avez tout fait correctement, alors nous devrions voir la réponse à notre demande get: ['Pizza', 'Coke'] .


Note du traducteur: pour créer de nouveaux contrôleurs, ainsi que d'autres éléments de nest.js : services, fournisseurs, etc., il est plus pratique d'utiliser la commande nest generate du nest-cli . Par exemple, pour créer le contrôleur décrit ci-dessus, vous pouvez utiliser la commande nest generate controller items , à la suite de quoi nest créera les src/items/items.controller.tc src/items/items.controller.spec.tc et src/items/items.controller.tc contenu suivant:


  import { Get, Post, Controller } from '@nestjs/common'; @Controller('items') export class ItemsController {} 

et enregistrez-le dans app.molule.tc


Ajout d'un service


Maintenant, lors de l'accès à /items notre application renvoie le même tableau pour chaque demande, que nous ne pouvons pas changer. Le traitement et la sauvegarde des données ne sont pas l'affaire du contrôleur; à cet effet, les services sont destinés à nest.js
Les services dans nest sont des @Injectable
Le nom du décorateur parle de lui-même, l'ajout de ce décorateur à la classe le rend injectable dans d'autres composants, tels que les contrôleurs.
Créons notre service. Créez le fichier items.service.ts dans le dossier items.service.ts avec le contenu suivant:


  import { Injectable } from '@nestjs/common'; @Injectable() export class ItemsService { private readonly items: string[] = ['Pizza', 'Coke']; findAll(): string[] { return this.items; } create(item: string) { this.items.push(item); } } 

et changez le contrôleur ItemsController (déclaré dans items.controller.ts ) pour utiliser notre service:


  import { Get, Post, Body, Controller } from '@nestjs/common'; import { ItemsService } from './items.service'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<string[]> { return this.itemsService.findAll(); } @Post() async create(@Body() item: string) { this.itemsService.create(item); } } 

Dans la nouvelle version du contrôleur, nous avons appliqué le décorateur @Body à l'argument de méthode create . Cet argument est utilisé pour faire correspondre automatiquement les données transmises via req.body ['item'] à l'argument lui-même (dans ce cas, item ).
Notre contrôleur reçoit également une instance de la classe ItemsService , injectée via le constructeur. Déclarer ItemsService comme private readonly rend une instance immuable et visible uniquement à l'intérieur de la classe.
Et n'oubliez pas d'enregistrer notre service dans app.module.ts :


  import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; import { ItemsService } from './items/items.service'; @Module({ imports: [], controllers: [ItemsController], providers: [ItemsService], }) export class AppModule {} 

Après toutes les modifications, envoyons une demande HTTP POST au menu:


  curl -X POST -H 'content-type: application/json' -d '{"item": "Salad"}' localhost:3000/items 

Ensuite, nous vérifierons si de nouveaux plats sont apparus dans notre menu en faisant une demande GET (ou en ouvrant http: // localhost: 3000 / items dans un navigateur)


  curl localhost:3000/items 

Création d'un itinéraire de panier d'achat


Maintenant que nous avons la première version du point d'entrée /items notre API, implémentons la fonctionnalité de panier. Le processus de création de cette fonctionnalité n'est pas très différent de l'API déjà créée. Par conséquent, afin de ne pas encombrer le manuel, nous allons créer un composant qui répond avec un statut OK lors de l'accès.


Tout d'abord, dans le dossier ./src/shopping-cart/ créez le shoping-cart.controller.ts :


  import { Post, Controller } from '@nestjs/common'; @Controller('shopping-cart') export class ShoppingCartController { @Post() async addItem() { return 'This is a fake service :D'; } } 

Enregistrez ce contrĂ´leur dans notre module ( app.module.ts ):


  import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; import { ShoppingCartController } from './shopping-cart/shopping-cart.controller'; import { ItemsService } from './items/items.service'; @Module({ imports: [], controllers: [ItemsController, ShoppingCartController], providers: [ItemsService], }) export class AppModule {} 

Pour vérifier ce point d'entrée, exécutez la commande suivante, après vous être assuré que l'application est en cours d'exécution:


  curl -X POST localhost:3000/shopping-cart 

Ajout d'un script d'interface pour les éléments


Retour à notre service d' items . Maintenant, nous enregistrons uniquement le nom du plat, mais ce n'est clairement pas suffisant, et, bien sûr, nous voudrons avoir plus d'informations (par exemple, le coût du plat). Je pense que vous conviendrez que le stockage de ces données sous forme de tableau de chaînes n'est pas une bonne idée?
Pour résoudre ce problème, nous pouvons créer un tableau d'objets. Mais comment sauvegarder la structure des objets? Ici, l'interface TypeScript nous aidera, dans laquelle nous définirons la structure de l'objet items . Créez un nouveau fichier nommé item.interface.ts dans le dossier src/items :


  export interface Items { readonly name: string; readonly price: number; } 

items.service.ts ensuite items.service.ts fichier items.service.ts :


 import { Injectable } from '@nestjs/common'; import { Item } from './item.interface'; @Injectable() export class ItemsService { private readonly items: Item[] = []; findAll(): Item[] { return this.items; } create(item: Item) { this.items.push(item); } } 

Et aussi dans items.controller.ts :


 import { Get, Post, Body, Controller } from '@nestjs/common'; import { ItemsService } from './items.service'; import { Item } from './item.interface'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<Item[]> { return this.itemsService.findAll(); } @Post() async create(@Body() item: Item) { this.itemsService.create(item); } } 

Validation de l'entrée dans Nest.js


Malgré le fait que nous ayons déterminé la structure de l'objet item , notre application ne retournera pas d'erreur si nous envoyons une requête POST invalide (tout type de données non défini dans l'interface). Par exemple, pour une telle demande:


  curl -H 'Content-Type: application/json' -d '{ "name": 3, "price": "any" }' http://localhost:3000/items 

le serveur doit répondre avec un état de 400 (mauvaise demande), mais à la place, notre application répondra avec un état de 200 (OK).


Pour résoudre ce problème, créez un DTO (Data Transfer Object) et un composant Pipe (canal).


DTO est un objet qui définit comment les données doivent être transférées entre les processus. Nous décrivons le DTO dans le src/items/create-item.dto.ts :


  import { IsString, IsInt } from 'class-validator'; export class CreateItemDto { @IsString() readonly name: string; @IsInt() readonly price: number; } 

Les tuyaux dans Nest.js sont les composants utilisés pour la validation. Pour notre API, créez un canal dans lequel il vérifie si les données envoyées à la méthode correspondent au DTO. Un canal peut être utilisé par différents contrôleurs, alors créez le répertoire src/common/ avec le fichier validation.pipe.ts :


  import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform, } from '@nestjs/common'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; @Injectable() export class ValidationPipe implements PipeTransform<any> { async transform(value, metadata: ArgumentMetadata) { const { metatype } = metadata; if (!metatype || !this.toValidate(metatype)) { return value; } const object = plainToClass(metatype, value); const errors = await validate(object); if (errors.length > 0) { throw new BadRequestException('Validation failed'); } return value; } private toValidate(metatype): boolean { const types = [String, Boolean, Number, Array, Object]; return !types.find(type => metatype === type); } } 

Remarque: Nous devons installer deux modules: class-validator class-transformer . Pour ce faire, exécutez npm install class-validator class-transformer dans la console et redémarrez le serveur.

Adaptation de items.controller.ts pour une utilisation avec notre nouveau tube et DTO:


  import { Get, Post, Body, Controller, UsePipes } from '@nestjs/common'; import { CreateItemDto } from './create-item.dto'; import { ItemsService } from './items.service'; import { Item } from './item.interface'; import { ValidationPipe } from '../common/validation.pipe'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<Item[]> { return this.itemsService.findAll(); } @Post() @UsePipes(new ValidationPipe()) async create(@Body() createItemDto: CreateItemDto) { this.itemsService.create(createItemDto); } } 

Vérifions à nouveau notre code, maintenant l'entrée /items n'accepte les données que si elles sont définies dans le DTO. Par exemple:


  curl -H 'Content-Type: application/json' -d '{ "name": "Salad", "price": 3 }' http://localhost:3000/items 

Collez des données non valides (données qui ne peuvent pas être vérifiées dans ValidationPipe ), par conséquent, nous obtenons la réponse:


  {"statusCode":400,"error":"Bad Request","message":"Validation failed"} 

Création de middleware

Selon la page du guide de démarrage rapide Auth0 , la méthode recommandée pour vérifier le jeton JWT émis par Auth0 consiste à utiliser le middleware Express fourni par express-jwt . Ce middleware automatise une grande partie du travail.


Créons un fichier authentication.middleware.ts dans le répertoire src / common avec le code suivant:


  import { NestMiddleware } from '@nestjs/common'; import * as jwt from 'express-jwt'; import { expressJwtSecret } from 'jwks-rsa'; export class AuthenticationMiddleware implements NestMiddleware { use(req, res, next) { jwt({ secret: expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: 'https://${DOMAIN}/.well-known/jwks.json', }), audience: 'http://localhost:3000', issuer: 'https://${DOMAIN}/', algorithm: 'RS256', })(req, res, err => { if (err) { const status = err.status || 500; const message = err.message || 'Sorry, we were unable to process your request.'; return res.status(status).send({ message, }); } next(); }); }; } 

Remplacez ${DOMAIN} par la valeur de domaine des paramètres de l'application Auth0


Note du traducteur: dans une application réelle, supprimez DOMAIN en une constante et définissez sa valeur via env (environnement virtuel)

Installez les jwks-rsa express-jwt et jwks-rsa :


  npm install express-jwt jwks-rsa 

Il est nécessaire de connecter le middleware créé (gestionnaire) à notre application. Pour ce faire, dans le fichier ./src/app.module.ts :


  import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; import { AuthenticationMiddleware } from './common/authentication.middleware'; import { ItemsController } from './items/items.controller'; import { ShoppingCartController } from './shopping-cart/shopping-cart.controller'; import { ItemsService } from './items/items.service'; @Module({ imports: [], controllers: [ItemsController, ShoppingCartController], providers: [ItemsService], }) export class AppModule { public configure(consumer: MiddlewareConsumer) { consumer .apply(AuthenticationMiddleware) .forRoutes( { path: '/items', method: RequestMethod.POST }, { path: '/shopping-cart', method: RequestMethod.POST }, ); } } 

Le code ci-dessus indique que les requêtes POST vers les itinéraires /items et /shopping-cart sont protégées par le middleware Express , qui vérifie le jeton d'accès dans la requête.


Redémarrez le serveur de développement ( npm run start:dev ) et appelez l'API Nest.js:


  #     curl -X POST http://localhost:3000/shopping-cart #      TOKEN="eyJ0eXAiO...Mh0dpeNpg" # and issue a POST request with it curl -X POST -H 'authorization: Bearer '$TOKEN http://localhost:3000/shopping-cart 

Gestion des rĂ´les avec Auth0

Pour le moment, tout utilisateur disposant d'un jeton vérifié peut publier un élément dans notre API. Cependant, nous aimerions que seuls les utilisateurs disposant de droits d'administrateur puissent le faire. Pour implémenter cette fonction, nous utilisons les règles (règles) Auth0 .


Alors, allez dans le panneau de configuration Auth0, dans la section Règles . Là, cliquez sur le bouton + CREATE RULE et sélectionnez "Définir les rôles pour un utilisateur" comme modèle de règle.



Cela fait, nous obtenons un fichier JavaScript avec un modèle de règle qui ajoute le rôle d'administrateur à tout utilisateur qui a un e-mail appartenant à un certain domaine. Modifions quelques détails dans ce modèle pour obtenir un exemple fonctionnel. Pour notre application, nous ne donnerons à l'administrateur qu'un accès à notre propre adresse e-mail. Nous devrons également modifier l'emplacement de stockage des informations sur le statut d'administrateur.


À l'heure actuelle, ces informations sont stockées dans un jeton d'identification (utilisé pour fournir des informations sur l'utilisateur), mais un jeton d'accès doit être utilisé pour accéder aux ressources de l'API. Le code après les modifications devrait ressembler à ceci:


  function (user, context, callback) { user.app_metadata = user.app_metadata || {}; if (user.email && user.email === '${YOUR_EMAIL}') { user.app_metadata.roles = ['admin']; } else { user.app_metadata.roles = ['user']; } auth0.users .updateAppMetadata(user.user_id, user.app_metadata) .then(function() { context.accessToken['http://localhost:3000/roles'] = user.app_metadata.roles; callback(null, user, context); }) .catch(function(err) { callback(err); }); } 

Remarque: remplacez ${YOUR_EMAIL} par votre adresse e-mail. Il est important de noter qu'en règle générale, lorsque vous traitez des e-mails dans les règles Auth0, il est idéal de forcer la vérification des e-mails . Dans ce cas, cela n'est pas obligatoire car nous utilisons notre propre adresse e-mail.

Note du traducteur: le fragment de code ci-dessus est entré dans le navigateur sur la page de configuration de la règle Auth0

Pour vérifier si le jeton transmis à notre API est le jeton administrateur, nous devons créer un gardien Nest.js. Dans le dossier src/common , créez le fichier admin.guard.ts


  import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; @Injectable() export class AdminGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const user = context.getArgs()[0].user['http://localhost:3000/roles'] || ''; return user.indexOf('admin') > -1; } } 

Maintenant, si nous répétons le processus de connexion décrit ci-dessus et utilisons l'adresse e-mail définie dans la règle, nous obtiendrons un nouveau access_token . Pour vérifier le contenu de ce access_token , copiez et collez le jeton dans le champ Encoded du site https://jwt.io/ . Nous verrons que la section de charge utile de ce jeton contient le tableau suivant:


  "http://localhost:3000/roles": [ "admin" ] 

Si notre jeton inclut vraiment ces informations, nous continuons l'intégration avec Auth0. Alors, ouvrez items.controller.ts et ajoutez notre nouvelle garde là-bas:


  import { Get, Post, Body, Controller, UsePipes, UseGuards, } from '@nestjs/common'; import { CreateItemDto } from './create-item.dto'; import { ItemsService } from './items.service'; import { Item } from './item.interface'; import { ValidationPipe } from '../common/validation.pipe'; import { AdminGuard } from '../common/admin.guard'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<Item[]> { return this.itemsService.findAll(); } @Post() @UseGuards(new AdminGuard()) @UsePipes(new ValidationPipe()) async create(@Body() createItemDto: CreateItemDto) { this.itemsService.create(createItemDto); } } 

Maintenant, avec notre nouveau jeton, nous pouvons ajouter de nouveaux éléments via notre API:


  #    npm run start:dev #  POST       curl -X POST -H 'Content-Type: application/json' \ -H 'authorization: Bearer '$TOKEN -d '{ "name": "Salad", "price": 3 }' http://localhost:3000/items 

Note du traducteur: pour vérification, vous pouvez voir ce que nous avons dans les articles:
 curl -X GET http://localhost:3000/items 


Résumé


Félicitations! Nous venons de terminer la construction de notre API Nest.JS et nous pouvons maintenant nous concentrer sur le développement de la partie frontale de notre application! N'oubliez pas de consulter la deuxième partie de cette série: Applications TypeScript à pile complète - Partie 2: Développement d'applications angulaires frontales.


Note du traducteur: La traduction de la deuxième partie est en cours.

Pour résumer, dans cet article, nous avons utilisé diverses fonctionnalités de Nest.js et TypeScript: modules, contrôleurs, services, interfaces, canaux, middleware et guard pour créer API J'espère que vous avez une bonne expérience et êtes prêt à continuer à développer notre application. Si quelque chose ne vous est pas clair, la documentation officielle nest.js est une bonne source de réponses

Source: https://habr.com/ru/post/fr471348/


All Articles