Création d'une API REST Node.js/TypeScript, partie 2 : modèles, middleware et services
Publié: 2022-03-11Dans le premier article de notre série d'API REST, nous avons expliqué comment utiliser npm pour créer un back-end à partir de zéro, ajouter des dépendances telles que TypeScript, utiliser le module de debug intégré à Node.js, créer une structure de projet Express.js et enregistrer l'exécution. événements flexibles avec Winston. Si vous êtes déjà à l'aise avec ces concepts, clonez simplement ceci, passez à la toptal-article-01 avec git checkout et lisez la suite.
Services d'API REST, intergiciels, contrôleurs et modèles
Comme promis, nous allons maintenant entrer dans les détails de ces modules :
- Des services qui rendent notre code plus propre en encapsulant des opérations de logique métier dans des fonctions que le middleware et les contrôleurs peuvent appeler.
- Middleware qui validera les conditions préalables avant qu'Express.js appelle la fonction de contrôleur appropriée.
- Les contrôleurs qui utilisent des services pour traiter la demande avant d'envoyer finalement une réponse au demandeur.
- Modèles qui décrivent nos données et facilitent les vérifications au moment de la compilation.
Nous ajouterons également une base de données très rudimentaire qui n'est en aucun cas adaptée à la production. (Son seul but est de rendre ce didacticiel plus facile à suivre, ouvrant la voie à notre prochain article pour approfondir la connexion à la base de données et l'intégration avec MongoDB et Mongoose.)
Travaux pratiques : Premiers pas avec les DAO, les DTO et notre base de données temporaire
Pour cette partie de notre tutoriel, notre base de données n'utilisera même pas de fichiers. Il conservera simplement les données utilisateur dans un tableau, ce qui signifie que les données s'évaporent chaque fois que nous quittons Node.js. Il ne prendra en charge que les opérations de création, de lecture, de mise à jour et de suppression (CRUD) les plus élémentaires.
Nous allons utiliser ici deux concepts :
- Objets d' accès aux données (DAO)
- Objets de transfert de données (DTO)
Cette différence d'une lettre entre les acronymes est essentielle : un DAO est chargé de se connecter à une base de données définie et d'effectuer des opérations CRUD ; un DTO est un objet qui contient les données brutes que le DAO enverra et recevra de la base de données.
En d'autres termes, les DTO sont des objets conformes aux types de modèles de données et les DAO sont les services qui les utilisent.
Bien que les DTO puissent devenir plus compliqués (représentant des entités de base de données imbriquées, par exemple), dans cet article, une seule instance de DTO correspondra à une action spécifique sur une seule ligne de base de données.
Pourquoi les DTO ?
L'utilisation de DTO pour que nos objets TypeScript soient conformes à nos modèles de données permet de maintenir la cohérence architecturale, comme nous le verrons dans la section sur les services ci-dessous. Mais il y a une mise en garde cruciale : ni les DTO ni TypeScript lui-même ne promettent une sorte de validation automatique des entrées utilisateur, car cela devrait se produire au moment de l'exécution. Lorsque notre code reçoit une entrée utilisateur à un point de terminaison dans notre API, cette entrée peut :
- Avoir des champs supplémentaires
- Manquer des champs obligatoires (c'est-à-dire ceux qui ne sont pas suffixés par
?) - Avoir des champs dans lesquels les données ne sont pas du type que nous avons spécifié dans notre modèle à l'aide de TypeScript
TypeScript (et le JavaScript vers lequel il est transpilé) ne vérifiera pas cela par magie pour nous, il est donc important de ne pas oublier ces validations, en particulier lors de l'ouverture de votre API au public. Des packages comme ajv peuvent aider à cela, mais fonctionnent normalement en définissant des modèles dans un objet de schéma spécifique à la bibliothèque plutôt que dans TypeScript natif. (Mongoose, discuté dans le prochain article, jouera un rôle similaire dans ce projet.)
Vous pensez peut-être : "Est-il vraiment préférable d'utiliser à la fois les DAO et les DTO, au lieu de quelque chose de plus simple ?" Le développeur d'entreprise Gunther Popp offre une réponse ; vous voudrez éviter les DTO dans la plupart des petits projets Express.js/TypeScript du monde réel, sauf si vous pouvez raisonnablement vous attendre à évoluer à moyen terme.
Mais même si vous n'êtes pas sur le point de les utiliser en production, cet exemple de projet est une opportunité intéressante sur la voie de la maîtrise de l'architecture de l'API TypeScript. C'est un excellent moyen de s'entraîner à exploiter les types TypeScript de manière supplémentaire et à travailler avec les DTO pour voir comment ils se comparent à une approche plus basique lors de l'ajout de composants et de modèles.
Notre modèle d'API REST utilisateur au niveau TypeScript
Nous allons d'abord définir trois DTO pour notre utilisateur. Créons un dossier appelé dto dans le dossier des users et créons-y un fichier appelé create.user.dto.ts contenant les éléments suivants :
export interface CreateUserDto { id: string; email: string; password: string; firstName?: string; lastName?: string; permissionLevel?: number; }Nous disons que chaque fois que nous créons un utilisateur, quelle que soit la base de données, il doit avoir un identifiant, un mot de passe et un e-mail, et éventuellement un prénom et un nom. Ces exigences peuvent changer en fonction des exigences commerciales d'un projet donné.
Pour les requêtes PUT , nous souhaitons mettre à jour l'intégralité de l'objet, nos champs optionnels sont donc désormais obligatoires. Dans le même dossier, créez un fichier nommé put.user.dto.ts avec ce code :
export interface PutUserDto { id: string; email: string; password: string; firstName: string; lastName: string; permissionLevel: number; } Pour les requêtes PATCH , nous pouvons utiliser la fonctionnalité Partial de TypeScript, qui crée un nouveau type en copiant un autre type et en rendant tous ses champs facultatifs. Ainsi, le fichier patch.user.dto.ts ne doit contenir que le code suivant :
import { PutUserDto } from './put.user.dto'; export interface PatchUserDto extends Partial<PutUserDto> {} Maintenant, créons la base de données temporaire en mémoire. Créons un dossier appelé daos dans le dossier des users et ajoutons un fichier nommé users.dao.ts .
Tout d'abord, nous voulons importer les DTO que nous avons créés :
import { CreateUserDto } from '../dto/create.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; import { PutUserDto } from '../dto/put.user.dto';Maintenant, pour gérer nos ID utilisateur, ajoutons la bibliothèque shortid (en utilisant le terminal) :
npm i shortid npm i --save-dev @types/shortid De retour dans users.dao.ts , nous importerons le shortid :
import shortid from 'shortid'; import debug from 'debug'; const log: debug.IDebugger = debug('app:in-memory-dao'); Nous pouvons maintenant créer une classe appelée UsersDao , qui ressemblera à ceci :
class UsersDao { users: Array<CreateUserDto> = []; constructor() { log('Created new instance of UsersDao'); } } export default new UsersDao(); En utilisant le modèle singleton, cette classe fournira toujours la même instance - et, surtout, le même tableau d' users - lorsque nous l'importerons dans d'autres fichiers. C'est parce que Node.js met en cache ce fichier partout où il est importé, et toutes les importations se produisent au démarrage. Autrement dit, tout fichier faisant référence à users.dao.ts recevra une référence au même new UsersDao() qui est exporté la première fois que Node.js traite ce fichier.
Nous verrons cela fonctionner lorsque nous utiliserons cette classe plus loin dans cet article et utiliserons ce modèle commun TypeScript/Express.js pour la plupart des classes tout au long du projet.
Remarque : Un inconvénient souvent cité des singletons est qu'il est difficile d'écrire des tests unitaires pour eux. Dans le cas de beaucoup de nos classes, cet inconvénient ne s'appliquera pas, puisqu'il n'y a pas de variables membres de classe qui auraient besoin d'être réinitialisées. Mais pour ceux où ce serait le cas, nous le laissons comme un exercice pour que le lecteur envisage d'aborder ce problème avec l'utilisation de l'injection de dépendances.
Nous allons maintenant ajouter les opérations CRUD de base à la classe en tant que fonctions. La fonction de création ressemblera à ceci :
async addUser(user: CreateUserDto) { user.id = shortid.generate(); this.users.push(user); return user.id; }La lecture se déclinera en deux versions, "lire toutes les ressources" et "lire une par ID". Ils sont codés comme ceci :
async getUsers() { return this.users; } async getUserById(userId: string) { return this.users.find((user: { id: string }) => user.id === userId); } De même, update signifiera soit écraser l'objet complet (comme un PUT ) ou seulement des parties de l'objet (comme un PATCH ):
async putUserById(userId: string, user: PutUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1, user); return `${user.id} updated via put`; } async patchUserById(userId: string, user: PatchUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); let currentUser = this.users[objIndex]; const allowedPatchFields = [ 'password', 'firstName', 'lastName', 'permissionLevel', ]; for (let field of allowedPatchFields) { if (field in user) { // @ts-ignore currentUser[field] = user[field]; } } this.users.splice(objIndex, 1, currentUser); return `${user.id} patched`; } Comme mentionné précédemment, malgré notre déclaration UserDto dans ces signatures de fonction, TypeScript ne fournit aucune vérification de type à l'exécution. Cela signifie que:
-
putUserById()a un bogue. Cela permettra aux consommateurs d'API de stocker des valeurs pour les champs qui ne font pas partie du modèle défini par notre DTO. -
patchUserById()dépend d'une liste en double de noms de champs qui doivent être synchronisés avec le modèle. Sans cela, il devrait utiliser l'objet mis à jour pour cette liste. Cela signifierait qu'il ignorerait silencieusement les valeurs des champs qui font partie du modèle défini par DTO mais qui n'avaient pas été enregistrés auparavant pour cette instance d'objet particulière.
Mais ces deux scénarios seront traités correctement au niveau de la base de données dans le prochain article.
La dernière opération, pour supprimer une ressource, ressemblera à ceci :
async removeUserById(userId: string) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1); return `${userId} removed`; }En bonus, sachant qu'un préalable pour créer un utilisateur est de valider si l'email de l'utilisateur n'est pas dupliqué, ajoutons maintenant une fonction "obtenir l'utilisateur par email" :
async getUserByEmail(email: string) { const objIndex = this.users.findIndex( (obj: { email: string }) => obj.email === email ); let currentUser = this.users[objIndex]; if (currentUser) { return currentUser; } else { return null; } }Remarque : Dans un scénario réel, vous vous connecterez probablement à une base de données à l'aide d'une bibliothèque préexistante, telle que Mongoose ou Sequelize, qui résumera toutes les opérations de base dont vous pourriez avoir besoin. De ce fait, nous n'entrons pas dans le détail des fonctions implémentées ci-dessus.
Notre couche de services API REST
Maintenant que nous avons un DAO de base en mémoire, nous pouvons créer un service qui appellera les fonctions CRUD. Étant donné que les fonctions CRUD sont quelque chose que chaque service qui se connectera à une base de données devra avoir, nous allons créer une interface CRUD qui contient les méthodes que nous voulons implémenter chaque fois que nous voulons implémenter un nouveau service.
De nos jours, les IDE avec lesquels nous travaillons ont des fonctionnalités de génération de code pour ajouter les fonctions que nous implémentons, réduisant ainsi la quantité de code répétitif que nous devons écrire.
Un exemple rapide utilisant l'IDE WebStorm :
L'IDE met en surbrillance le nom de la classe MyService et suggère les options suivantes :
L'option "Implémenter tous les membres" échafaude instantanément les fonctions nécessaires pour se conformer à l'interface CRUD :
Cela dit, créons d'abord notre interface TypeScript, appelée CRUD . Dans notre dossier common , créons un dossier appelé interfaces et ajoutons crud.interface.ts avec ce qui suit :
export interface CRUD { list: (limit: number, page: number) => Promise<any>; create: (resource: any) => Promise<any>; putById: (id: string, resource: any) => Promise<string>; readById: (id: string) => Promise<any>; deleteById: (id: string) => Promise<string>; patchById: (id: string, resource: any) => Promise<string>; } Cela fait, créons un dossier de services dans le dossier des users et ajoutez-y le fichier users.service.ts , contenant :
import UsersDao from '../daos/users.dao'; import { CRUD } from '../../common/interfaces/crud.interface'; import { CreateUserDto } from '../dto/create.user.dto'; import { PutUserDto } from '../dto/put.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; class UsersService implements CRUD { async create(resource: CreateUserDto) { return UsersDao.addUser(resource); } async deleteById(id: string) { return UsersDao.removeUserById(id); } async list(limit: number, page: number) { return UsersDao.getUsers(); } async patchById(id: string, resource: PatchUserDto) { return UsersDao.patchUserById(id, resource); } async readById(id: string) { return UsersDao.getUserById(id); } async putById(id: string, resource: PutUserDto) { return UsersDao.putUserById(id, resource); } async getUserByEmail(email: string) { return UsersDao.getUserByEmail(email); } } export default new UsersService(); Notre première étape ici a été d'importer notre DAO en mémoire, notre dépendance d'interface et le type TypeScript de chacun de nos DTO, il est temps d'implémenter UsersService en tant que singleton de service, le même modèle que nous avons utilisé avec notre DAO.
Toutes les fonctions CRUD appellent maintenant simplement les fonctions respectives de UsersDao . Lorsque viendra le temps de remplacer le DAO, nous n'aurons pas à apporter de modifications ailleurs dans le projet, à l'exception de quelques ajustements à ce fichier où les fonctions DAO sont appelées, comme nous le verrons dans la partie 3.
Par exemple, nous n'aurons pas à rechercher chaque appel à list() et à vérifier son contexte avant de le remplacer. C'est l'avantage d'avoir cette couche de séparation, au prix de la petite quantité de passe-partout initial que vous voyez ci-dessus.
Async/Attente et Node.js
Notre utilisation d' async pour les fonctions de service peut sembler inutile. Pour l'instant, c'est le cas : toutes ces fonctions renvoient immédiatement leurs valeurs, sans aucune utilisation interne de Promise s ou d' await . Il s'agit uniquement de préparer notre base de code pour les services qui utiliseront async . De même, ci-dessous, vous verrez que tous les appels à ces fonctions utilisent await .
À la fin de cet article, vous aurez à nouveau un projet exécutable à expérimenter. Ce sera un excellent moment pour essayer d'ajouter différents types d'erreurs à différents endroits de la base de code et de voir ce qui se passe pendant la compilation et les tests. Les erreurs dans un contexte async en particulier peuvent ne pas se comporter comme prévu. Cela vaut la peine de creuser et d'explorer diverses solutions, qui sortent du cadre de cet article.

Maintenant que notre DAO et nos services sont prêts, revenons au contrôleur utilisateur.
Construire notre contrôleur d'API REST
Comme nous l'avons dit plus haut, l'idée derrière les contrôleurs est de séparer la configuration de la route du code qui traite finalement une demande de route. Cela signifie que toutes les validations doivent être effectuées avant que notre demande n'atteigne le contrôleur. Le contrôleur a seulement besoin de savoir quoi faire avec la demande réelle, car si la demande est arrivée jusque-là, nous savons qu'elle s'est avérée valide. Le responsable du traitement appellera alors le service respectif de chaque demande qu'il traitera.
Avant de commencer, nous devrons installer une bibliothèque pour hacher en toute sécurité le mot de passe de l'utilisateur :
npm i argon2 Commençons par créer un dossier appelé controllers dans le dossier du contrôleur des users et y créons un fichier appelé users.controller.ts :
// we import express to add types to the request/response objects from our controller functions import express from 'express'; // we import our newly created user services import usersService from '../services/users.service'; // we import the argon2 library for password hashing import argon2 from 'argon2'; // we use debug with a custom context as described in Part 1 import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersController { async listUsers(req: express.Request, res: express.Response) { const users = await usersService.list(100, 0); res.status(200).send(users); } async getUserById(req: express.Request, res: express.Response) { const user = await usersService.readById(req.body.id); res.status(200).send(user); } async createUser(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); const userId = await usersService.create(req.body); res.status(201).send({ id: userId }); } async patch(req: express.Request, res: express.Response) { if (req.body.password) { req.body.password = await argon2.hash(req.body.password); } log(await usersService.patchById(req.body.id, req.body)); res.status(204).send(); } async put(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); log(await usersService.putById(req.body.id, req.body)); res.status(204).send(); } async removeUser(req: express.Request, res: express.Response) { log(await usersService.deleteById(req.body.id)); res.status(204).send(); } } export default new UsersController(); Remarque : les lignes ci-dessus ne renvoyant rien avec une réponse HTTP 204 No Content sont conformes à la RFC 7231 sur le sujet.
Une fois notre singleton de contrôleur utilisateur terminé, nous sommes prêts à coder l'autre module qui dépend de notre exemple de modèle d'objet et de service d'API REST : notre middleware utilisateur.
Middleware Node.js REST avec Express.js
Que pouvons-nous faire avec le middleware Express.js ? Les validations sont un excellent choix, d'une part. Ajoutons quelques validations de base pour agir en tant que gardiens des requêtes avant qu'elles ne soient transmises à notre contrôleur utilisateur :
- S'assurer de la présence de champs d'utilisateur tels que l'adresse e-
emailet lepasswordde passe requis pour créer ou mettre à jour un utilisateur - Assurez-vous qu'un e-mail donné n'est pas déjà utilisé
- Vérifiez que nous ne modifions pas le champ de l'e-
emailaprès la création (puisque nous l'utilisons comme identifiant principal pour l'utilisateur pour plus de simplicité) - Valider si un utilisateur donné existe
Pour que ces validations fonctionnent avec Express.js, nous devrons les traduire en fonctions qui suivent le modèle Express.js de contrôle de flux à l'aide de next() , comme décrit dans l'article précédent. Nous aurons besoin d'un nouveau fichier, users/middleware/users.middleware.ts :
import express from 'express'; import userService from '../services/users.service'; import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersMiddleware { } export default new UsersMiddleware();Avec le passe-partout singleton familier à l'écart, ajoutons certaines de nos fonctions middleware au corps de la classe :
async validateRequiredUserBodyFields( req: express.Request, res: express.Response, next: express.NextFunction ) { if (req.body && req.body.email && req.body.password) { next(); } else { res.status(400).send({ error: `Missing required fields email and password`, }); } } async validateSameEmailDoesntExist( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user) { res.status(400).send({ error: `User email already exists` }); } else { next(); } } async validateSameEmailBelongToSameUser( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user && user.id === req.params.userId) { next(); } else { res.status(400).send({ error: `Invalid email` }); } } // Here we need to use an arrow function to bind `this` correctly validatePatchEmail = async ( req: express.Request, res: express.Response, next: express.NextFunction ) => { if (req.body.email) { log('Validating email', req.body.email); this.validateSameEmailBelongToSameUser(req, res, next); } else { next(); } }; async validateUserExists( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.readById(req.params.userId); if (user) { next(); } else { res.status(404).send({ error: `User ${req.params.userId} not found`, }); } } Pour permettre à nos consommateurs d'API de faire facilement d'autres requêtes concernant un utilisateur nouvellement ajouté, nous allons ajouter une fonction d'assistance qui extraira l' userId utilisateur des paramètres de la requête - provenant de l'URL de la requête elle-même - et l'ajoutera au corps de la requête, où résident le reste des données utilisateur.
L'idée ici est de pouvoir simplement utiliser la requête complète du corps lorsque nous souhaitons mettre à jour les informations de l'utilisateur, sans nous soucier d'obtenir l'ID des paramètres à chaque fois. Au lieu de cela, il est pris en charge à un seul endroit, le middleware. La fonction ressemblera à ceci :
async extractUserId( req: express.Request, res: express.Response, next: express.NextFunction ) { req.body.id = req.params.userId; next(); } Outre la logique, la principale différence entre le middleware et le contrôleur est que nous utilisons maintenant la fonction next() pour passer le contrôle le long d'une chaîne de fonctions configurées jusqu'à ce qu'il arrive à la destination finale, qui dans notre cas est le contrôleur.
Tout mettre ensemble : refactoriser nos itinéraires
Maintenant que nous avons implémenté tous les nouveaux aspects de notre architecture de projet, revenons au fichier users.routes.config.ts que nous avons défini dans l'article précédent. Il appellera notre middleware et nos contrôleurs, qui reposent tous deux sur notre service utilisateur, qui à son tour nécessite notre modèle utilisateur.
Le fichier final sera aussi simple que ceci :
import { CommonRoutesConfig } from '../common/common.routes.config'; import UsersController from './controllers/users.controller'; import UsersMiddleware from './middleware/users.middleware'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes(): express.Application { this.app .route(`/users`) .get(UsersController.listUsers) .post( UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailDoesntExist, UsersController.createUser ); this.app.param(`userId`, UsersMiddleware.extractUserId); this.app .route(`/users/:userId`) .all(UsersMiddleware.validateUserExists) .get(UsersController.getUserById) .delete(UsersController.removeUser); this.app.put(`/users/:userId`, [ UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailBelongToSameUser, UsersController.put, ]); this.app.patch(`/users/:userId`, [ UsersMiddleware.validatePatchEmail, UsersController.patch, ]); return this.app; } } Ici, nous avons redéfini nos routes en ajoutant un middleware pour valider notre logique métier et les fonctions de contrôleur appropriées pour traiter la demande si tout est valide. Nous avons également utilisé la fonction .param() de Express.js pour extraire le userId .
Au niveau de la fonction .all() , nous transmettons notre fonction validateUserExists de UsersMiddleware pour qu'elle soit appelée avant que tout GET , PUT , PATCH ou DELETE puisse passer sur le point de terminaison /users/:userId . Cela signifie que validateUserExists n'a pas besoin d'être dans les tableaux de fonctions supplémentaires que nous passons à .put() ou .patch() — il sera appelé avant les fonctions qui y sont spécifiées.
Nous avons également tiré parti de la réutilisation inhérente du middleware d'une autre manière. En passant UsersMiddleware.validateRequiredUserBodyFields à utiliser dans les contextes POST et PUT , nous le recombinons élégamment avec d'autres fonctions middleware.
Avis de non-responsabilité : nous ne couvrons que les validations de base dans cet article. Dans un projet réel, vous devrez réfléchir et trouver toutes les restrictions dont vous avez besoin pour coder. Par souci de simplicité, nous supposons également qu'un utilisateur ne peut pas modifier son adresse e-mail.
Test de notre API REST Express/TypeScript
Nous pouvons maintenant compiler et exécuter notre application Node.js. Une fois qu'il est en cours d'exécution, nous sommes prêts à tester nos routes API à l'aide d'un client REST tel que Postman ou cURL.
Essayons d'abord d'obtenir nos utilisateurs :
curl --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json'À ce stade, nous aurons un tableau vide comme réponse, ce qui est précis. Maintenant, nous pouvons essayer de créer la première ressource utilisateur avec ceci :
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json'Notez que maintenant notre application Node.js renverra une erreur de notre middleware :
{ "error": "Missing required fields email and password" } Pour résoudre ce problème, envoyons une demande valide de publication à la ressource /users :
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23" }'Cette fois, nous devrions voir quelque chose comme ceci :
{ "id": "ksVnfnPVW" } Cet id est l'identifiant de l'utilisateur nouvellement créé et sera différent sur votre machine. Pour faciliter les instructions de test restantes, vous pouvez exécuter cette commande avec celle que vous obtenez (en supposant que vous utilisez un environnement de type Linux) :
REST_API_EXAMPLE_ Nous pouvons maintenant voir la réponse que nous obtenons en faisant une requête GET en utilisant la variable ci-dessus :
curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' Nous pouvons désormais également mettre à jour l'intégralité de la ressource avec la requête PUT suivante :
curl --request PUT "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23", "firstName": "Marcos", "lastName": "Silva", "permissionLevel": 8 }'Nous pouvons également tester que notre validation fonctionne en changeant l'adresse e-mail, ce qui devrait entraîner une erreur.
Notez que lors de l'utilisation d'un PUT à un ID de ressource, nous, en tant que consommateurs d'API, devons envoyer l'objet entier si nous voulons nous conformer au modèle REST standard. Cela signifie que si nous voulons mettre à jour uniquement le champ lastName , mais en utilisant notre point de terminaison PUT , nous serons obligés d'envoyer l'objet entier à mettre à jour. Il serait plus facile d'utiliser une requête PATCH car il est toujours dans les contraintes REST standard d'envoyer uniquement le champ lastName :
curl --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "lastName": "Faraco" }' Rappelez-vous que dans notre propre base de code, c'est notre configuration de route qui applique cette distinction entre PUT et PATCH en utilisant les fonctions middleware que nous avons ajoutées dans cet article.
PUT , PATCH ou les deux ?
Il peut sembler qu'il n'y a pas beaucoup de raisons de prendre en charge PUT étant donné la flexibilité de PATCH , et certaines API adopteront cette approche. D'autres peuvent insister pour prendre en charge PUT pour rendre l'API "complètement conforme à REST", auquel cas, la création de routes PUT par champ peut être une tactique appropriée pour les cas d'utilisation courants.
En réalité, ces points font partie d'une discussion beaucoup plus large allant des différences réelles entre les deux à une sémantique plus flexible pour PATCH seul. Nous présentons ici le support PUT et la sémantique PATCH largement utilisée pour plus de simplicité, mais encourageons les lecteurs à approfondir leurs recherches lorsqu'ils se sentent prêts à le faire.
En récupérant la liste des utilisateurs comme nous l'avons fait ci-dessus, nous devrions voir notre utilisateur créé avec ses champs mis à jour :
[ { "id": "ksVnfnPVW", "email": "[email protected]", "password": "$argon2i$v=19$m=4096,t=3,p=1$ZWXdiTgb922OvkNAdh9acA$XUXsOHaRN4uVg5ltIwwO+SPLxvb9uhOKcxoLER1e/mM", "firstName": "Marcos", "lastName": "Faraco", "permissionLevel": 8 } ]Enfin, nous pouvons tester la suppression de l'utilisateur avec ceci :
curl --request DELETE "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json'En récupérant la liste des utilisateurs, nous devrions voir que l'utilisateur supprimé n'est plus présent.
Avec cela, nous avons toutes les opérations CRUD pour la ressource users qui fonctionnent.
API REST Node.js/TypeScript
Dans cette partie de la série, nous avons approfondi les étapes clés de la création d'une API REST à l'aide d'Express.js. Nous divisons notre code pour prendre en charge les services, les intergiciels, les contrôleurs et les modèles. Chacune de leurs fonctions a un rôle spécifique, qu'il s'agisse de validation, d'opérations logiques ou de traitement de demandes valides et d'y répondre.
Nous avons également créé un moyen très simple de stocker des données, dans le but exprès (pardonnez le jeu de mots) de permettre certains tests à ce stade, puis d'être remplacé par quelque chose de plus pratique dans la prochaine partie de notre série.
Outre la création d'une API dans un souci de simplicité, à l'aide de classes singleton, par exemple, il existe plusieurs étapes à suivre pour la rendre plus facile à maintenir, plus évolutive et sécurisée. Dans le dernier article de la série, nous couvrons :
- Remplacement de la base de données en mémoire par MongoDB, puis utilisation de Mongoose pour simplifier le processus de codage
- Ajouter une couche de sécurité et contrôler l'accès dans une approche sans état avec JWT
- Configuration des tests automatisés pour permettre à notre application d'évoluer
Vous pouvez parcourir le code final de cet article ici.
