Der erste Arbeitsplatz oder wie man mit der Entwicklung der API auf Node.js beginnt

Einführung


In diesem Artikel möchte ich meine Emotionen und erworbenen Fähigkeiten bei der Entwicklung der ersten REST-API auf Node.js mit TypeScript von Grund auf teilen. Die Geschichte ist ziemlich banal: „Ich habe mein Studium abgeschlossen und ein Diplom erhalten. Wohin zur Arbeit gehen? " Wie Sie vielleicht vermutet haben, ging das Problem nicht an mir vorbei, obwohl ich nicht zu viel nachdenken musste. Der Entwickler (Absolvent derselben Fachrichtung) forderte ein Praktikum an. Ich glaube, dass dies eine ziemlich verbreitete Praxis ist und es viele ähnliche Geschichten gibt. Ohne nachzudenken, beschloss ich, mich zu versuchen und ging ...

Bild

Erster Tag. Wir stellen vor: Node.js.


Ich bin zur Backend-Entwicklung gekommen. Dieses IT-Unternehmen verwendet die Node.js- Plattform, mit der ich überhaupt nicht vertraut war. Ich lief ein wenig vorwärts und vergaß dem Leser zu sagen, dass ich nie etwas in JavaScript entwickelt hatte (außer ein paar Skripten mit Kopiercode). Im Allgemeinen verstand ich den Arbeitsalgorithmus und die Architektur von Webanwendungen, da ich CRUD in Java, Python und Clojure entwickelte, aber dies war nicht genug. Daher hat dieser Screencast am ersten Tag, an dem ich mich ganz dem Studium von Node.js widmete, wirklich geholfen.

Während ich das Express- Webframework, den npm- Paketmanager sowie Dateien wie package.json und tsconfig.json studierte, ging mein Kopf einfach um die Informationsmenge herum. Eine weitere Lektion ist, dass es nahezu unmöglich ist, das gesamte Material gleichzeitig zu beherrschen. Am Ende des Tages konnte ich die Umgebung immer noch konfigurieren und den Express-Webserver ausführen! Aber es war zu früh, um sich zu freuen, denn er ging mit einem vollen Gefühl des Missverständnisses nach Hause. Das Gefühl, in der riesigen Welt von JS zu ertrinken, ließ mich keine Minute stehen, so dass ein Neustart erforderlich war.

Zweiter Tag. Einführung in TypeScript


Der gleiche Neustart folgte noch am selben Tag. An diesem Punkt habe ich mein Problem vollständig erkannt, wir werden etwas weiter darauf eingehen. In dem Wissen, dass es nicht notwendig war, in reinem JavaScipt zu schreiben, floss das Training von Node.js reibungslos in die TypeScript-Sprache, nämlich deren Funktionen und Syntax. Hier sah ich die lang erwarteten Typen , ohne die die Programmierung buchstäblich vor 2 Tagen erfolgte, nicht in funktionalen Programmiersprachen. Dies war mein größtes Missverständnis, das mich daran hinderte, den in JavaScript geschriebenen Code am ersten Tag zu verstehen und zu lernen.

Zuvor schrieb er größtenteils in objektorientierten Programmiersprachen wie Java, C ++, C #. Als ich die Möglichkeiten von TypeScript erkannte, fühlte ich mich wohl. Diese Programmiersprache hauchte mir buchstäblich das Leben dieser komplexen Umgebung ein, wie es mir damals schien. Gegen Ende des Tages habe ich die Umgebung vollständig eingerichtet, den Server (bereits in TypeScript) gestartet und die erforderlichen Bibliotheken verbunden, auf die ich weiter unten eingehen werde. Fazit: Bereit zur Entwicklung der API. Wir gehen direkt zur Entwicklung über ...

API-Entwicklung


Eine Erklärung des Arbeitsprinzips und andere Erklärungen der REST-API werden wir hinterlassen, da das Forum viele Artikel mit Beispielen und Entwicklungen in verschiedenen Programmiersprachen enthält.
Bild

Die Aufgabe war wie folgt:

Erstellen Sie einen Service mit einer REST-API. Autorisierung durch Inhaber-Token (/ info, / latency, / logout). Konfiguriertes CORS für den Zugriff von jeder Domain aus. DB - MongoDB. Erstellen Sie bei jedem Aufruf ein Token.

API-Beschreibung:

  1. / signin [POST] - Token-Träger nach ID und Passwort anfordern // Daten in JSON empfangen
  2. / signup [POST] - Registrierung eines neuen Benutzers: // empfängt Daten in json
  3. / info [GET] - Gibt die Benutzer-ID und den ID-Typ zurück und erfordert das vom Inhaber bei der Authentifizierung ausgestellte Token
  4. / latency [GET] - Gibt eine Verzögerung (Ping) zurück und erfordert das vom Inhaber bei der Authentifizierung ausgestellte Token
  5. / logout [GET] - mit dem Parameter all: true - löscht alle Benutzer-Bearer-Token oder false - löscht nur das aktuelle Bearer-Token

Ich stelle sofort fest, dass die Aufgabe für einen Webanwendungsentwickler unglaublich einfach aussieht. Aber die Aufgabe muss in einer Programmiersprache implementiert werden, von der vor 3 Tagen überhaupt nichts wusste! Selbst für mich sieht es auf dem Papier völlig transparent aus und in Python hat die Implementierung etwas Zeit in Anspruch genommen, aber ich hatte keine solche Option. Der Entwicklungsstapel deutete auf Probleme hin.

Mittel zur Umsetzung


Daher habe ich erwähnt, dass ich am zweiten Tag, an dem ich bereits mehrere Bibliotheken (Frameworks) studiert habe, damit beginnen werde. Für das Routing habe ich Routing-Controller ausgewählt , die sich an vielen Ähnlichkeiten mit Dekorateuren aus dem Spring Framework (Java) orientieren. Als ORM habe ich mich für typeorm entschieden, obwohl ich mit MongoDB im experimentellen Modus arbeite, ist es für eine solche Aufgabe völlig ausreichend. Ich habe uuid verwendet , um Token zu generieren. Variablen werden mit dotenv geladen.

Start des Webservers


Normalerweise wird Express in seiner reinen Form verwendet, aber ich habe das Routing Controller-Framework erwähnt, mit dem wir einen Express-Server wie folgt erstellen können:

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


Wie Sie sehen können, gibt es nichts Kompliziertes. Tatsächlich verfügt das Framework über viel mehr Funktionen, die jedoch nicht benötigt wurden.
  • routePrefix ist nur ein Präfix in Ihrer URL nach der Serveradresse, zum Beispiel: localhost : 3000 / Präfix
  • Standardeinstellungen - nichts Interessantes, initialisieren Sie einfach die Fehlercodes
  • authorisationChecker - eine großartige Gelegenheit für das Framework, die Benutzerautorisierung zu überprüfen, dann werden wir genauer darauf eingehen
  • Controller ist eines der Hauptfelder, in denen wir die in unserer Anwendung verwendeten Controller angeben


DB-Verbindung


Zuvor hatten wir den Webserver bereits gestartet, daher werden wir weiterhin eine Verbindung zur MongoDB-Datenbank herstellen, nachdem wir ihn zuvor auf dem lokalen Server bereitgestellt haben. Installation und Konfiguration sind in der offiziellen Dokumentation ausführlich beschrieben. Wir werden die Verbindung direkt mit typeorm betrachten:

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


Alles ist ganz einfach, Sie müssen mehrere Parameter angeben:

  • Typ - DB
  • Host - IP-Adresse, an der Sie die Datenbank bereitgestellt haben
  • Datenbank - Der Name der Datenbank, die zuvor in Mongodb erstellt wurde
  • synchronisieren - automatische Synchronisation mit der Datenbank (Hinweis: Es war zu diesem Zeitpunkt schwierig, die Migration zu meistern)
  • Entitäten - hier geben wir die Entitäten an, mit denen die Synchronisation durchgeführt wird


Jetzt verbinden wir den Start des Servers und die Verbindung zur Datenbank. Ich stelle fest, dass sich der Import von Ressourcen von dem in Node.js verwendeten klassischen unterscheidet. Als Ergebnis erhalten wir die folgende ausführbare Datei, in meinem Fall 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äten


Ich möchte Sie daran erinnern, dass die Aufgabe darin besteht, Benutzer zu authentifizieren bzw. zu autorisieren. Wir benötigen eine Entität: Benutzer. Das ist aber noch nicht alles, denn jeder Benutzer hat einen Token und keinen! Daher muss eine Token-Entität erstellt werden.

Benutzer

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

In der Benutzertabelle erstellen wir ein Feld - ein Array der Token für den Benutzer. Wir aktivieren auch den Calss-Validator , da sich der Benutzer per E-Mail anmelden muss.

Token

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

Die Basis ist wie folgt:

Bild

Benutzerautorisierung


Zur Autorisierung verwenden wir authorisationChecker (einer der Parameter beim Erstellen des Servers, siehe oben). Der Einfachheit halber legen wir ihn in einer separaten Datei ab:

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

Nach der Authentifizierung hat jeder Benutzer sein eigenes Token, sodass wir das erforderliche Token aus den Headern der Antwort abrufen können . Es sieht ungefähr so aus: Träger 046a5f60-c55e-11e9-af71-c75526de439e . Jetzt können wir überprüfen, ob dieses Token vorhanden ist. Danach gibt die Funktion Autorisierungsinformationen zurück: true - der Benutzer ist autorisiert, false - der Benutzer ist nicht autorisiert. In der Anwendung können wir einen sehr praktischen Dekorator in der Steuerung verwenden: @Authorized (). Zu diesem Zeitpunkt wird die Funktion authorisationChecker aufgerufen, die eine Antwort zurückgibt.

Logik


Zunächst möchte ich die Geschäftslogik beschreiben, da der Controller eine Zeile von Methodenaufrufen unterhalb der dargestellten Klasse ist. Außerdem akzeptieren wir im Controller alle Daten, in unserem Fall JSON und Query. Wir werden die Methoden für einzelne Aufgaben betrachten und am Ende die endgültige Datei mit dem Namen UserService.ts erstellen. Ich stelle fest, dass es zu dieser Zeit einfach nicht genug Wissen gab, um Abhängigkeiten zu beseitigen. Wenn Sie den Begriff Abhängigkeitsinjektion nicht kennengelernt haben, empfehle ich dringend, darüber zu lesen. Im Moment verwende ich das DI-Framework, dh ich verwende Container, nämlich die Injektion durch Konstruktoren. Hier, denke ich, ist ein guter Artikel zur Überprüfung. Wir kehren zur Aufgabe zurück.

  • / signin [POST] - Authentifizierung des registrierten Benutzers. Alles ist sehr einfach und transparent. Wir müssen nur diesen Benutzer in der Datenbank finden und ein neues Token ausstellen. Zum Lesen und Schreiben wird MongoRepository verwendet.

     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] - Registrieren Sie einen neuen Benutzer. Eine sehr ähnliche Methode, da wir zunächst auch einen Benutzer suchen, damit wir keine Benutzer mit einer E-Mail registriert haben. Als nächstes schreiben wir den neuen Benutzer in die Datenbank, nachdem wir das Token ausgestellt haben.

     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] - Gibt die Benutzer-ID und den ID-Typ zurück und erfordert das vom Inhaber bei der Authentifizierung ausgestellte Token. Das Bild ist auch transparent: Zuerst erhalten wir das aktuelle Token des Benutzers aus den Anforderungsheadern, suchen es dann in der Datenbank und bestimmen, wem es gehört, und geben den gefundenen Benutzer zurück.

     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] - Gibt eine Verzögerung (Ping) zurück und erfordert das vom Inhaber bei der Authentifizierung ausgestellte Token. Ein völlig uninteressanter Absatz des Artikels. Hier habe ich nur eine vorgefertigte Bibliothek zum Überprüfen der TCP-Ping-Verzögerung verwendet.

     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] - mit dem Parameter all: true - löscht alle Benutzer-Bearer-Token oder false - löscht nur das aktuelle Bearer-Token. Wir müssen nur den Benutzer finden, den Abfrageparameter überprüfen und die Token entfernen. Ich denke, alles sollte klar sein.

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


Controller


Viele müssen nicht erklären, was benötigt wird und wie der Controller im MVC-Muster verwendet wird, aber ich werde trotzdem zwei Wörter sagen. Kurz gesagt, der Controller ist die Verbindung zwischen dem Benutzer und der Anwendung, die Daten zwischen ihnen umleitet. Die Logik wurde oben vollständig beschrieben, deren Methoden gemäß den Routen aufgerufen werden und aus einem URI und einem IP-Server bestehen (Beispiel: localhost: 3000 / signin) . Ich habe bereits über Dekorateure im Controller gesprochen: Get , POST , @Authorized und das wichtigste davon ist @JsonController. Ein weiteres sehr wichtiges Merkmal dieses Frameworks ist, dass wir, wenn wir JSON senden und empfangen möchten, diesen Dekorator anstelle von Controller verwenden .

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

Fazit


In diesem Artikel wollte ich nicht mehr die technische Komponente des richtigen Codes oder ähnliches widerspiegeln, sondern einfach die Tatsache teilen, dass eine Person eine Webanwendung mithilfe einer Datenbank erstellen kann und in fünf Tagen mindestens eine Logik von einer absoluten Null enthält. Denken Sie nur darüber nach, kein Instrument war Ihnen vertraut, erinnern Sie sich an sich selbst oder setzen Sie es einfach an meine Stelle. In keinem Fall ist dies der Fall, der sagt: "Ich bin der Beste, das kann man nie tun." Im Gegenteil, dies ist ein Schrei aus der Seele einer Person, die derzeit völlig begeistert von der Welt von Node.js ist und dies mit Ihnen teilt. Und die Tatsache, dass nichts unmöglich ist, müssen Sie nur nehmen und tun!

Natürlich kann nicht geleugnet werden, dass der Autor nichts wusste und sich zum ersten Mal hinsetzte, um Code zu schreiben. Nein, Kenntnisse über OOP, die Prinzipien der REST-API, ORM und der Datenbank waren in ausreichenden Mengen vorhanden. Und das kann nur sagen, dass das Mittel zum Erreichen des Ergebnisses absolut keine Rolle spielt und im Stil sagt: "Ich werde nicht zu diesem Job gehen, es gibt eine Programmiersprache, die ich nicht gelernt habe", für mich ist es jetzt nur die Manifestation einer Person, nicht von Schwäche, sondern vielmehr Schutz vor einer unbekannten äußeren Umgebung. Aber was gibt es zu verbergen, die Angst war bei mir vorhanden.

Zusammenfassend. Ich möchte Studenten und Menschen, die ihre Karriere in der IT noch nicht begonnen haben, raten, keine Angst vor Entwicklungstools und unbekannten Technologien zu haben. Ältere Genossen werden Ihnen sicherlich helfen (wenn Sie genauso viel Glück haben wie ich), sie werden es Ihnen ausführlich erklären und Fragen beantworten, da jeder von ihnen in dieser Position war. Aber vergessen Sie nicht, dass Ihr Wunsch der wichtigste Aspekt ist!

Link zum Projekt

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


All Articles