Le premier lieu de travail ou comment commencer à développer une API sur Node.js

Présentation


Dans cet article, je voudrais partager mes émotions et mes compétences acquises dans le développement de la première API REST sur Node.js en utilisant TypeScript, comme on dit, à partir de zéro. L'histoire est assez banale: «J'ai obtenu mon diplôme universitaire, j'ai reçu un diplôme. Où aller travailler? " Comme vous l'avez peut-être deviné, le problème ne m'a pas échappé, même si je n'ai pas eu à trop réfléchir. Le développeur (diplômé de la même spécialité) a demandé un stage. Je pense que c'est une pratique assez courante et il y a beaucoup d'histoires similaires. Sans réfléchir à deux fois, j'ai décidé de m'essayer et je suis allé ...

image

Premier jour. Présentation de Node.js


Je suis venu au développement back-end. Cette société informatique utilise la plateforme Node.js , que je ne connaissais pas du tout. J'ai couru un peu en avant, oubliant de dire au lecteur que je n'avais jamais rien développé en JavaScript (à l'exception de quelques scripts avec du code de copie). En général, j'ai compris l'algorithme de travail et l'architecture des applications web, depuis que j'ai développé CRUD en Java, Python et Clojure, mais ce n'était pas suffisant. Par conséquent, le premier jour que j'ai entièrement consacré à l'étude de Node.js, ce screencast a vraiment aidé.

En étudiant le framework Web Express , le gestionnaire de packages npm , ainsi que des fichiers tels que package.json et tsconfig.json, ma tête a simplement fait le tour de la quantité d'informations. Une autre leçon est que maîtriser tout le matériel en même temps est presque une tâche impossible. À la fin de la journée, j'ai quand même réussi à configurer l'environnement et j'ai pu exécuter le serveur Web express! Mais il était trop tôt pour se réjouir, car il rentra chez lui avec un sentiment d'incompréhension total. Le sentiment que je me noyais dans le vaste monde de JS ne me quitta pas une minute, donc un redémarrage était nécessaire.

Deuxième jour. Présentation de TypeScript


Ce même redémarrage a suivi le jour même. À ce stade, j'ai pleinement reconnu mon problème, nous allons y passer un peu plus bas. Sachant qu'il n'était pas nécessaire d'écrire en JavaScipt pur, la formation de Node.js s'est déroulée en douceur vers le langage TypeScript, à savoir ses fonctionnalités et sa syntaxe. Ici, j'ai vu les types tant attendus, sans lesquels la programmation était littéralement il y a 2 jours, pas dans les langages de programmation fonctionnels. Ce fut ma plus grande idée fausse, ce qui m'a empêché de comprendre et d'apprendre le code écrit en JavaScript le premier jour.

Il a précédemment écrit pour la plupart dans des langages de programmation orientés objet tels que Java, C ++, C #. Réalisant les possibilités de TypeScript, je me suis senti à l'aise. Ce langage de programmation m'a littéralement insufflé la vie de cet environnement complexe, comme il me semblait à l'époque. Vers la fin de la journée, j'ai complètement configuré l'environnement, lancé le serveur (déjà sur TypeScript), connecté les bibliothèques nécessaires, dont je parlerai ci-dessous. Conclusion: prêt à développer l'API. On passe directement au développement ...

Développement d'API


Une explication du principe de travail et d'autres explications de ce qu'est l'API REST, nous partirons, car le forum a beaucoup d'articles à ce sujet avec des exemples et des développements dans différents langages de programmation.
image

La tâche était la suivante:

Créez un service avec une API REST. Autorisation par jeton au porteur (/ info, / latence, / déconnexion). CORS configuré pour l'accès à partir de n'importe quel domaine. DB - MongoDB. Créez un jeton à chaque appel.

Description de l'API:

  1. / signin [POST] - demande le porteur de jeton par identifiant et mot de passe // reçoit les données dans json
  2. / signup [POST] - enregistrement d'un nouvel utilisateur: // reçoit les données dans json
  3. / info [GET] - renvoie l'ID utilisateur et le type d'ID, nécessite le jeton émis par le porteur lors de l'authentification
  4. / latency [GET] - renvoie un délai (ping), nécessite le jeton émis par le porteur lors de l'authentification
  5. / logout [GET] - avec le paramètre all: true - supprime tous les jetons de porteur d'utilisateur ou false - supprime uniquement le jeton de porteur actuel

Je remarque tout de suite, la tâche semble incroyablement simple pour un développeur d'applications Web. Mais la tâche doit être implémentée dans un langage de programmation, dont il y a 3 jours ne savait rien du tout! Même pour moi, cela semble complètement transparent sur papier et en Python, l'implémentation a pris un peu de temps, mais je n'avais pas une telle option. La pile de développement a signalé des problèmes.

Moyens de mise en œuvre


Donc, j'ai mentionné que le deuxième jour, j'ai déjà étudié plusieurs bibliothèques (frameworks), nous allons commencer par là. Pour le routage, j'ai choisi des contrôleurs de routage , guidés par de nombreuses similitudes avec les décorateurs du Spring Framework (Java). En tant qu'ORM , j'ai choisi la typeorm , bien que travaillant avec MongoDB en mode expérimental, c'est assez pour une telle tâche. J'ai utilisé uuid pour générer des jetons, les variables sont chargées en utilisant dotenv .

Démarrage du serveur Web


Habituellement, express est utilisé dans sa forme pure, mais j'ai mentionné le framework Routing Controllers, qui nous permet de créer un serveur express comme suit:

//  Express const app = createExpressServer({ // routePrefix: process.env.SERVER_PREFIX, //  defaults: { nullResultCode: Number(process.env.ERROR_NULL_RESULT_CODE), undefinedResultCode: Number(process.env.ERROR_NULL_UNDEFINED_RESULT_CODE), paramOptions: { required: true } }, //   authorizationChecker: authorizationChecker, // controllers: [UserController] }); //  app.listen(process.env.SERVER_PORT, () => { console.log(process.env.SERVER_MASSAGE); }); 


Comme vous pouvez le voir, il n'y a rien de compliqué. En fait, le cadre a beaucoup plus de fonctionnalités, mais il n'y en avait pas besoin.
  • routePrefix est juste un préfixe dans votre URL après l'adresse du serveur, par exemple: localhost : 3000 / prefix
  • valeurs par défaut - rien d'intéressant, il suffit d'initialiser les codes d'erreur
  • autorisationChecker - une excellente occasion pour le cadre de vérifier l'autorisation de l'utilisateur, alors nous examinerons plus en détail
  • contrôleurs est l'un des principaux domaines où nous spécifions les contrôleurs utilisés dans notre application


Connexion DB


Auparavant, nous avions déjà lancé le serveur Web, nous continuerons donc de nous connecter à la base de données MongoDB, après l'avoir précédemment déployé sur le serveur local. L'installation et la configuration sont décrites en détail dans la documentation officielle . Nous considérerons directement la connexion à l'aide de typeorm:

 //  createConnection({ type: 'mongodb', host: process.env.DB_HOST, database: process.env.DB_NAME_DATABASE, entities: [ User ], synchronize: true, logging: false }).catch(error => console.log(error)); 


Tout est assez simple, vous devez spécifier plusieurs paramètres:

  • type - DB
  • hôte - adresse IP où vous avez déployé la base de données
  • base de données - le nom de la base de données qui a été précédemment créée dans mongodb
  • synchronize - synchronisation automatique avec la base de données (Remarque: il était difficile de maîtriser la migration à ce moment-là)
  • entités - nous indiquons ici les entités avec lesquelles la synchronisation est effectuée


Maintenant, nous connectons le démarrage du serveur et la connexion à une base de données. Je note que l'importation de ressources est différente de celle classique utilisée dans Node.js. En conséquence, nous obtenons le fichier exécutable suivant, dans mon cas main.ts:

 import 'reflect-metadata'; import * as dotenv from 'dotenv'; import { createExpressServer } from 'routing-controllers'; import { createConnection } from 'typeorm'; import { authorizationChecker } from './auth/authorizationChecker'; import { UserController } from './controllers/UserController'; import { User } from './models/User'; dotenv.config(); //  createConnection({ type: 'mongodb', host: process.env.DB_HOST, database: process.env.DB_NAME_DATABASE, entities: [ User ], synchronize: true, logging: false }).catch(error => console.log(error)); //  Express const app = createExpressServer({ // routePrefix: process.env.SERVER_PREFIX, //  defaults: { nullResultCode: Number(process.env.ERROR_NULL_RESULT_CODE), undefinedResultCode: Number(process.env.ERROR_NULL_UNDEFINED_RESULT_CODE), paramOptions: { required: true } }, //   authorizationChecker: authorizationChecker, // controllers: [UserController] }); //  app.listen(process.env.SERVER_PORT, () => { console.log(process.env.SERVER_MASSAGE); }); 

Entités


Permettez-moi de vous rappeler que la tâche consiste à authentifier et à autoriser les utilisateurs, respectivement, nous avons besoin d'une entité: Utilisateur. Mais ce n'est pas tout, puisque chaque utilisateur a un jeton et pas un! Par conséquent, il est nécessaire de créer une entité Token.

Utilisateur

 import { ObjectID } from 'bson'; import { IsEmail, MinLength } from 'class-validator'; import { Column, Entity, ObjectIdColumn } from 'typeorm'; import { Token } from './Token'; //  @Entity() export class User { //  @ObjectIdColumn() id: ObjectID; //Email    @Column() @IsEmail() email: string; //  @Column({ length: 100 }) @MinLength(2) password: string; //  @Column() token: Token; } 

Dans la table User, nous créons un champ - un tableau des jetons mêmes pour l'utilisateur. Nous activons également calss-validator , car il est nécessaire que l'utilisateur se connecte par e-mail.

Jeton

 import { Column, Entity } from 'typeorm'; //   @Entity() export class Token { @Column() accessToken: string; @Column() refreshToken: string; @Column() timeKill: number; } 

La base est la suivante:

image

Autorisation utilisateur


Pour l'autorisation, nous utilisons autorisationChecker (l'un des paramètres lors de la création du serveur, voir ci-dessus), pour plus de commodité, nous le mettons dans un fichier séparé:

 import { Action, UnauthorizedError } from 'routing-controllers'; import { getMongoRepository } from 'typeorm'; import { User } from '../models/User'; export async function authorizationChecker(action: Action): Promise<boolean> { let token: string; if (action.request.headers.authorization) { //   token = action.request.headers.authorization.split(" ", 2); const repository = getMongoRepository(User); const allUsers = await repository.find(); for (let i = 0; i < allUsers.length; i++) { if (allUsers[i].token.accessToken.toString() === token[1]) { return true; } } } else { throw new UnauthorizedError('This user has not token.'); } return false; } 

Après l'authentification, chaque utilisateur a son propre jeton, afin que nous puissions obtenir le jeton nécessaire dans les en-têtes de la réponse, il ressemble à ceci: Bearer 046a5f60-c55e-11e9-af71-c75526de439e . Nous pouvons maintenant vérifier si ce jeton existe, après quoi la fonction renvoie des informations d'autorisation: vrai - l'utilisateur est autorisé, faux - l'utilisateur n'est pas autorisé. Dans l'application, nous pouvons utiliser un décorateur très pratique dans le contrôleur: @Authorized (). À ce stade, la fonction autorisationChecker sera appelée, qui retournera une réponse.

La logique


Pour commencer, je voudrais décrire la logique métier, car le contrôleur est une ligne d'appels de méthode en dessous de la classe présentée. De plus, dans le contrôleur, nous accepterons toutes les données, dans notre cas, ce sera JSON et Query. Nous examinerons les méthodes pour les tâches individuelles, et à la fin, nous formerons le fichier final, qui s'appelle UserService.ts. Je note qu'à cette époque, il n'y avait tout simplement pas assez de connaissances pour éliminer les dépendances. Si vous n'avez pas rencontré le terme injection de dépendance, je vous recommande fortement de le lire. Pour le moment, j'utilise le framework DI, c'est-à-dire que j'utilise des conteneurs, à savoir l'injection via des constructeurs. Voici, je pense, un bon article à réviser. Nous reprenons la tâche.

  • / signin [POST] - authentification de l'utilisateur enregistré. Tout est très simple et transparent. Nous avons juste besoin de trouver cet utilisateur dans la base de données et d'émettre un nouveau jeton. Pour la lecture et l'écriture, MongoRepository est utilisé.

     async userSignin(user: User): Promise<string> { // Mongo repository const repo = getMongoRepository(User); //       let userEmail = await repo.findOne({ email: user.email, password: user.password }); if (userEmail) { //  userEmail = await this.setToken(userEmail); //    repo.save(userEmail); return userEmail.token.accessToken; } return process.env.USER_SERVICE_RESPONSE; } 
  • / signup [POST] - enregistre un nouvel utilisateur. Une méthode très similaire, car au début, nous recherchons également un utilisateur afin de ne pas avoir d'utilisateurs enregistrés avec un seul e-mail. Ensuite, nous écrivons le nouvel utilisateur dans la base de données, après avoir émis le jeton.

     async userSignup(newUser: User): Promise<string> { // Mongo repository const repo = getMongoRepository(User); //   email (   2    email) const userRepeat = await repo.findOne({ email: newUser.email }); if (!userRepeat) { //  newUser = await this.setToken(newUser); //   const addUser = getMongoManager(); await addUser.save(newUser); return newUser.token.accessToken; } else { return process.env.USER_SERVICE_RESPONSE; } } 
  • / info [GET] - renvoie l'ID utilisateur et le type d'ID, nécessite le jeton émis par le porteur lors de l'authentification. L'image est également transparente: nous obtenons d'abord le jeton actuel de l'utilisateur à partir des en-têtes de demande, puis le cherchons dans la base de données et déterminons à qui il se trouve, et renvoyons l'utilisateur trouvé.

     async getUserInfo(req: express.Request): Promise<User> { // Mongo repository const repository = getMongoRepository(User); //    const user = await this.findUser(req, repository); return user; } private async findUser(req: express.Request, repository: MongoRepository<User>): Promise<User> { if (req.get(process.env.HEADER_AUTH)) { //  const token = req.get(process.env.HEADER_AUTH).split(' ', 2); //    const usersAll = await repository.find(); //  for (let i = 0; i < usersAll.length; i++) { if (usersAll[i].token.accessToken.toString() === token[1]) { return usersAll[i]; } } } } 

  • / latency [GET] - renvoie un délai (ping), nécessite le jeton émis par le porteur lors de l'authentification. Un paragraphe complètement inintéressant de l'article, cependant. Ici, j'ai utilisé juste une bibliothèque prête à l'emploi pour vérifier le délai tcp-ping.

     getLatency(): Promise<IPingResult> { function update(progress: number, total: number): void { console.log(progress, '/', total); } const latency = ping({ address: process.env.PING_ADRESS, attempts: Number(process.env.PING_ATTEMPTS), port: Number(process.env.PING_PORT), timeout: Number(process.env.PING_TIMEOUT) }, update).then(result => { console.log('ping result:', result); return result; }); return latency; } 
  • / logout [GET] - avec le paramètre all: true - supprime tous les jetons de porteur d'utilisateur ou false - supprime uniquement le jeton de porteur actuel. Il nous suffit de trouver l'utilisateur, de vérifier le paramètre de requête et de supprimer les jetons. Je pense que tout devrait être clair.

     async userLogout(all: boolean, req: express.Request): Promise<void> { // Mongo repository const repository = getMongoRepository(User); //    const user = await this.findUser(req, repository); if (all) { // true    user.token.accessToken = process.env.GET_LOGOUT_TOKEN; user.token.refreshToken = process.env.GET_LOGOUT_TOKEN; //  repository.save(user); } else { // false    user.token.accessToken = process.env.GET_LOGOUT_TOKEN; //  repository.save(user); } } 


Contrôleur


Beaucoup n'ont pas besoin d'expliquer ce qui est nécessaire et comment le contrôleur est utilisé dans le modèle MVC, mais je dirai quand même deux mots. En bref, le contrôleur est le lien entre l'utilisateur et l'application qui redirige les données entre eux. Ci-dessus, la logique a été complètement décrite, dont les méthodes sont appelées en fonction des routes, consistant en un URI et un serveur ip (exemple: localhost: 3000 / signin) . J'ai mentionné plus tôt les décorateurs dans le contrôleur: Get , POST , @Authorized et le plus important d'entre eux est @JsonController. Une autre caractéristique très importante de ce framework est que si nous voulons envoyer et recevoir du JSON, alors nous utilisons ce décorateur au lieu de Controller .

 import * as express from 'express'; import { Authorized, Body, Get, Header, JsonController, NotFoundError, Post, QueryParam, Req, UnauthorizedError } from 'routing-controllers'; import { IPingResult } from '@network-utils/tcp-ping'; import { User } from '../models/User'; import { UserService } from '../services/UserService'; //    JSON @JsonController() export class UserController { userService: UserService //  constructor() { this.userService = new UserService(); } //  @Post('/signin') async login(@Body() user: User): Promise<string> { const responseSignin = await this.userService.userSignin(user); if (responseSignin !== process.env.USER_SERVICE_RESPONSE) { return responseSignin; } else { throw new NotFoundError(process.env.POST_SIGNIN_MASSAGE); } } //  @Post('/signup') async registrateUser(@Body() newUser: User): Promise<string> { const responseSignup = await this.userService.userSignup(newUser); if (responseSignup !== process.env.USER_SERVICE_RESPONSE) { return responseSignup; } else { throw new UnauthorizedError(process.env.POST_SIGNUP_MASSAGE); } } //   @Get('/info') @Authorized() async getId(@Req() req: express.Request): Promise<User> { return this.userService.getUserInfo(req); } //   @Authorized() @Get('/latency') getPing(): Promise<IPingResult> { return this.userService.getLatency(); } @Get('/logout') async deleteToken(@QueryParam("all") all: boolean, @Req() req: express.Request): Promise<void> { this.userService.userLogout(all, req); } } 

Conclusion


Dans cet article, je voulais refléter non plus la composante technique du code correct ou quelque chose comme ça, mais simplement partager le fait qu'une personne peut créer une application Web en utilisant une base de données et contenant au moins une logique à partir d'un zéro absolu en cinq jours. Pensez-y, aucun instrument n'était familier, souvenez-vous de vous ou mettez-le simplement à ma place. Ce n'est en aucun cas le cas qui dit: "Je suis le meilleur, tu ne peux jamais faire ça." Au contraire, c'est un cri de l'âme d'une personne qui est actuellement complètement ravie du monde de Node.js et qui partage cela avec vous. Et le fait que rien n'est impossible, il suffit de prendre et de faire!

Bien sûr, on ne peut nier que l'auteur ne savait rien et s'était assis pour écrire du code pour la première fois. Non, la connaissance de la POO, des principes de l'API REST, de l'ORM et de la base de données était présente en quantité suffisante. Et cela ne peut que dire que le moyen d’atteindre le résultat ne joue absolument aucun rôle et dire dans le style: "Je n’irai pas à ce métier, il y a un langage de programmation que je n’ai pas appris", pour moi maintenant c’est juste la manifestation d’une personne non pas de faiblesse, mais plutôt protection contre un environnement extérieur inconnu. Mais qu'y a-t-il à cacher, la peur était présente en moi.

Pour résumer. Je veux conseiller aux étudiants et aux personnes qui n'ont pas encore commencé leur carrière en informatique, à ne pas avoir peur des outils de développement et des technologies inconnues. Des camarades seniors vous aideront sûrement (si vous avez de la chance comme moi), ils vous expliqueront en détail et répondront aux questions, car chacun d'eux était dans cette position. Mais n'oubliez pas que votre désir est l'aspect le plus important!

Lien vers le projet

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


All Articles