Erste Schritte mit einem Express- und ES6+-JavaScript-Stack

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ Eine grundlegende Einführung in den Entwicklungsprozess von Back-End-Webanwendungen mit Express – Erörterung modernster JavaScript-Funktionen von ES6+, des Factory Design Pattern, MongoDB CRUD-Vorgänge, Server und Ports sowie die Zukunft mit n-Tier-Architekturmustern für TypeScript-Projekte in Unternehmen.

Dieser Artikel ist der zweite Teil einer Reihe, wobei Teil eins hier zu finden ist und grundlegende und (hoffentlich) intuitive Einblicke in Node.js, ES6+ JavaScript, Callback-Funktionen, Pfeilfunktionen, APIs, das HTTP-Protokoll, JSON, MongoDB und mehr bietet mehr.

In diesem Artikel bauen wir auf den Fähigkeiten auf, die wir im vorherigen Artikel erworben haben, und lernen, wie man eine MongoDB-Datenbank zum Speichern von Benutzerbuchlisteninformationen implementiert und bereitstellt, eine API mit Node.js und dem Express-Webanwendungsframework erstellt, um diese Datenbank verfügbar zu machen und CRUD-Operationen darauf ausführen und mehr. Unterwegs werden wir die ES6-Objektdestrukturierung, die ES6-Objekt-Kurzschrift, die Async/Await-Syntax, den Spread-Operator besprechen und einen kurzen Blick auf CORS, die Same-Origin-Richtlinie und mehr werfen.

In einem späteren Artikel werden wir unsere Codebasis umgestalten, um Bedenken zu trennen, indem wir eine dreischichtige Architektur verwenden und eine Umkehrung der Kontrolle über Dependency Injection erreichen Passwörter speichern und AWS Simple Storage Service verwenden, um Benutzer-Avatare mit Node.js-Puffern und -Streams zu speichern – während PostgreSQL für die Datenpersistenz verwendet wird. Unterwegs werden wir unsere Codebasis von Grund auf in TypeScript neu schreiben, um klassische OOP-Konzepte (wie Polymorphismus, Vererbung, Komposition usw.) und sogar Entwurfsmuster wie Fabriken und Adapter zu untersuchen.

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Ein Wort der Warnung

Es gibt ein Problem mit den meisten Artikeln, in denen es heute um Node.js geht. Die meisten von ihnen, nicht alle, gehen nicht weiter als zu zeigen, wie man Express Routing einrichtet, Mongoose integriert und vielleicht JSON Web Token Authentication verwendet. Das Problem ist, dass sie nicht über Architektur oder Best Practices für Sicherheit oder über saubere Codierungsprinzipien oder ACID-Compliance, relationale Datenbanken, fünfte Normalform, das CAP-Theorem oder Transaktionen sprechen. Es wird entweder davon ausgegangen, dass Sie über all das Bescheid wissen, oder dass Sie keine Projekte bauen, die groß oder populär genug sind, um das oben genannte Wissen zu rechtfertigen.

Es scheint ein paar verschiedene Arten von Node-Entwicklern zu geben – unter anderem sind einige neu in der Programmierung im Allgemeinen, und andere kommen aus einer langen Geschichte der Unternehmensentwicklung mit C# und dem .NET Framework oder dem Java Spring Framework. Die Mehrzahl der Artikel richtet sich an die erstgenannte Gruppe.

In diesem Artikel werde ich genau das tun, was ich gerade gesagt habe, was zu viele Artikel tun, aber in einem Folgeartikel werden wir unsere Codebasis vollständig umgestalten, sodass ich Prinzipien wie Dependency Injection, Three- Layer-Architektur (Controller/Service/Repository), Data Mapping und Active Record, Entwurfsmuster, Unit-, Integrations- und Mutationstests, SOLID-Prinzipien, Unit of Work, Codierung gegen Schnittstellen, Best Practices für Sicherheit wie HSTS, CSRF, NoSQL und SQL-Injection Prävention und so weiter. Wir werden auch von MongoDB zu PostgreSQL migrieren, indem wir den einfachen Abfrageersteller Knex anstelle eines ORM verwenden – was es uns ermöglicht, unsere eigene Datenzugriffsinfrastruktur aufzubauen und mit der strukturierten Abfragesprache, den verschiedenen Arten von Beziehungen (One- to-One, Many-to-Many usw.) und mehr. Dieser Artikel sollte also Anfänger ansprechen, aber die nächsten paar sollten sich an fortgeschrittenere Entwickler richten, die ihre Architektur verbessern möchten.

In diesem Fall kümmern wir uns nur um die Speicherung von Buchdaten. Wir kümmern uns nicht um Benutzerauthentifizierung, Passwort-Hashing, Architektur oder ähnliches Komplexes. All das wird in den nächsten und zukünftigen Artikeln kommen. Im Moment werden wir ganz einfach eine Methode erstellen, mit der es einem Client ermöglicht wird, mit unserem Webserver über das HTTP-Protokoll zu kommunizieren, um Buchinformationen in einer Datenbank zu speichern.

Anmerkung : Ich habe es hier absichtlich sehr einfach und vielleicht nicht so praktisch gehalten, weil dieser Artikel an und für sich sehr lang ist, da ich mir erlaubt habe, abzuweichen, um ergänzende Themen zu diskutieren. Daher werden wir die Qualität und Komplexität der API im Laufe dieser Serie schrittweise verbessern, aber da ich dies als eine Ihrer ersten Einführungen in Express betrachte, halte ich die Dinge absichtlich extrem einfach.

  1. ES6-Objektdestrukturierung
  2. ES6-Objekt-Kurzschrift
  3. ES6 Spread-Operator (...)
  4. Kommt ...

ES6-Objektdestrukturierung

ES6 Object Destructuring oder Destructuring Assignment Syntax ist eine Methode, mit der Werte aus Arrays oder Objekten in ihre eigenen Variablen extrahiert oder entpackt werden. Wir beginnen mit Objekteigenschaften und besprechen dann Array-Elemente.

 const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; // Log properties: console.log('Name:', person.name); console.log('Occupation:', person.occupation);

Eine solche Operation ist ziemlich primitiv, kann aber ziemlich mühsam sein, wenn man bedenkt, dass wir immer und überall auf person.something verweisen müssen. Angenommen, es gäbe 10 andere Stellen in unserem Code, an denen wir das tun müssten – es würde ziemlich schnell ziemlich mühsam werden. Eine Methode der Kürze wäre, diese Werte ihren eigenen Variablen zuzuweisen.

 const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; const personName = person.name; const personOccupation = person.occupation; // Log properties: console.log('Name:', personName); console.log('Occupation:', personOccupation);

Vielleicht sieht das vernünftig aus, aber was wäre, wenn wir auch 10 andere Eigenschaften im person verschachtelt hätten? Das wären viele unnötige Zeilen, nur um Variablen Werte zuzuweisen – an diesem Punkt sind wir in Gefahr, denn wenn Objekteigenschaften verändert werden, spiegeln unsere Variablen diese Änderung nicht wider (denken Sie daran, dass nur Verweise auf das Objekt mit const Zuweisung unveränderlich sind, nicht die Eigenschaften des Objekts), also können wir im Grunde den „Zustand“ (und ich verwende dieses Wort nur lose) nicht mehr synchron halten. Pass-by-Reference vs. Pass-by-Value könnten hier ins Spiel kommen, aber ich möchte nicht zu weit vom Rahmen dieses Abschnitts abweichen.

Mit ES6 Object Destructing können wir im Wesentlichen Folgendes tun:

 const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; // This is new. It's called Object Destructuring. const { name, occupation } = person; // Log properties: console.log('Name:', name); console.log('Occupation:', occupation);

Wir erstellen kein neues Objekt/Objektliteral, wir entpacken die name und occupation aus dem ursprünglichen Objekt und fügen sie in ihre eigenen gleichnamigen Variablen ein. Die von uns verwendeten Namen müssen mit den Eigenschaftsnamen übereinstimmen, die wir extrahieren möchten.

Auch hier gilt die Syntax const { a, b } = someObject; bedeutet ausdrücklich, dass wir erwarten, dass einige Eigenschaften a und einige Eigenschaften b innerhalb someObject vorhanden sind (dh someObject könnte beispielsweise { a: 'dataA', b: 'dataB' } ) und dass wir die Werte platzieren möchten dieser Schlüssel/Eigenschaften innerhalb const Variablen mit demselben Namen. Aus diesem Grund würde uns die obige Syntax zwei Variablen liefern const a = someObject.a und const b = someObject.b .

Das bedeutet, dass die Objektdestrukturierung zwei Seiten hat. Die Seite „Vorlage“ und die Seite „Quelle“, wobei die Seite const { a, b } (die linke Seite) die Vorlage und die Seite someObject (die rechte Seite) die Quellseite ist – was sinnvoll ist — Wir definieren links eine Struktur oder „Vorlage“, die die Daten auf der „Quellenseite“ widerspiegelt.

Um dies noch einmal deutlich zu machen, hier ein paar Beispiele:

 // ----- Destructure from Object Variable with const ----- // const objOne = { a: 'dataA', b: 'dataB' }; // Destructure const { a, b } = objOne; console.log(a); // dataA console.log(b); // dataB // ----- Destructure from Object Variable with let ----- // let objTwo = { c: 'dataC', d: 'dataD' }; // Destructure let { c, d } = objTwo; console.log(c); // dataC console.log(d); // dataD // Destructure from Object Literal with const ----- // const { e, f } = { e: 'dataE', f: 'dataF' }; // <-- Destructure console.log(e); // dataE console.log(f); // dataF // Destructure from Object Literal with let ----- // let { g, h } = { g: 'dataG', h: 'dataH' }; // <-- Destructure console.log(g); // dataG console.log(h); // dataH

Spiegeln Sie im Fall von verschachtelten Eigenschaften dieselbe Struktur in Ihrer zerstörenden Zuweisung:

 const person = { name: 'Richard P. Feynman', occupation: { type: 'Theoretical Physicist', location: { lat: 1, lng: 2 } } }; // Attempt one: const { name, occupation } = person; console.log(name); // Richard P. Feynman console.log(occupation); // The entire `occupation` object. // Attempt two: const { occupation: { type, location } } = person; console.log(type); // Theoretical Physicist console.log(location) // The entire `location` object. // Attempt three: const { occupation: { location: { lat, lng } } } = person; console.log(lat); // 1 console.log(lng); // 2

Wie Sie sehen können, sind die Eigenschaften, die Sie abrufen möchten, optional, und um verschachtelte Eigenschaften zu entpacken, spiegeln Sie einfach die Struktur des ursprünglichen Objekts (der Quelle) in der Vorlagenseite Ihrer Destrukturierungssyntax wider. Wenn Sie versuchen, eine Eigenschaft zu destrukturieren, die auf dem ursprünglichen Objekt nicht vorhanden ist, ist dieser Wert undefiniert.

Wir können eine Variable zusätzlich destrukturieren, ohne sie vorher zu deklarieren – Zuweisung ohne Deklaration – mit der folgenden Syntax:

 let name, occupation; const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; ;({ name, occupation } = person); console.log(name); // Richard P. Feynman console.log(occupation); // Theoretical Physicist

Wir stellen dem Ausdruck ein Semikolon voran, um sicherzustellen, dass wir nicht versehentlich einen IIFE (Immediately Invoked Function Expression) mit einer Funktion in einer vorherigen Zeile erstellen (falls eine solche Funktion existiert), und die Klammern um die Zuweisungsanweisung sind erforderlich Verhindern Sie, dass JavaScript Ihre linke Seite (Vorlage) als Block behandelt.

Ein sehr häufiger Anwendungsfall der Destrukturierung existiert innerhalb von Funktionsargumenten:

 const config = { baseUrl: '<baseURL>', awsBucket: '<bucket>', secret: '<secret-key>' // <- Make this an env var. }; // Destructures `baseUrl` and `awsBucket` off `config`. const performOperation = ({ baseUrl, awsBucket }) => { fetch(baseUrl).then(() => console.log('Done')); console.log(awsBucket); // <bucket> }; performOperation(config);

Wie Sie sehen können, hätten wir einfach die normale Destrukturierungssyntax verwenden können, an die wir jetzt innerhalb der Funktion gewöhnt sind, wie folgt:

 const config = { baseUrl: '<baseURL>', awsBucket: '<bucket>', secret: '<secret-key>' // <- Make this an env var. }; const performOperation = someConfig => { const { baseUrl, awsBucket } = someConfig; fetch(baseUrl).then(() => console.log('Done')); console.log(awsBucket); // <bucket> }; performOperation(config);

Aber wenn Sie diese Syntax in die Funktionssignatur einfügen, wird die Destrukturierung automatisch durchgeführt und wir sparen eine Zeile.

Ein realer Anwendungsfall dafür ist in React Functional Components for props :

 import React from 'react'; // Destructure `titleText` and `secondaryText` from `props`. export default ({ titleText, secondaryText }) => ( <div> <h1>{titleText}</h1> <h3>{secondaryText}</h3> </div> );

Im Gegensatz zu:

 import React from 'react'; export default props => ( <div> <h1>{props.titleText}</h1> <h3>{props.secondaryText}</h3> </div> );

In beiden Fällen können wir auch Standardwerte für die Eigenschaften festlegen:

 const personOne = { name: 'User One', password: 'BCrypt Hash' }; const personTwo = { password: 'BCrypt Hash' }; const createUser = ({ name = 'Anonymous', password }) => { if (!password) throw new Error('InvalidArgumentException'); console.log(name); console.log(password); return { id: Math.random().toString(36) // <--- Should follow RFC 4122 Spec in real app. .substring(2, 15) + Math.random() .toString(36).substring(2, 15), name: name, // <-- We'll discuss this next. password: password // <-- We'll discuss this next. }; } createUser(personOne); // User One, BCrypt Hash createUser(personTwo); // Anonymous, BCrypt Hash

Wie Sie sehen können, stellen wir für den Fall, dass der name bei der Destrukturierung nicht vorhanden ist, einen Standardwert bereit. Wir können dies auch mit der vorherigen Syntax tun:

 const { a, b, c = 'Default' } = { a: 'dataA', b: 'dataB' }; console.log(a); // dataA console.log(b); // dataB console.log(c); // Default

Arrays können auch destrukturiert werden:

 const myArr = [4, 3]; // Destructuring happens here. const [valOne, valTwo] = myArr; console.log(valOne); // 4 console.log(valTwo); // 3 // ----- Destructuring without assignment: ----- // let a, b; // Destructuring happens here. ;([a, b] = [10, 2]); console.log(a + b); // 12

Ein praktischer Grund für die Destrukturierung von Arrays tritt bei React Hooks auf. (Und es gibt viele andere Gründe, ich verwende React nur als Beispiel).

 import React, { useState } from "react"; export default () => { const [buttonText, setButtonText] = useState("Default"); return ( <button onClick={() => setButtonText("Toggled")}> {buttonText} </button> ); }

Beachten Sie, dass useState aus dem Export destrukturiert wird und die Array-Funktionen/-Werte aus dem useState Hook destrukturiert werden. Machen Sie sich keine Sorgen, wenn das Obige keinen Sinn ergibt – Sie müssten React verstehen – und ich benutze es nur als Beispiel.

Obwohl die Objektdestrukturierung in ES6 mehr beinhaltet, werde ich hier ein weiteres Thema behandeln: Destrukturierende Umbenennung, die nützlich ist, um Bereichskollisionen oder Variablenschatten usw. zu verhindern. Angenommen, wir möchten eine Eigenschaft namens name von einem Objekt namens person destrukturieren, aber Es gibt bereits eine Variable mit dem Namen name im Geltungsbereich. Wir können mit einem Doppelpunkt spontan umbenennen:

 // JS Destructuring Naming Collision Example: const name = 'Jamie Corkhill'; const person = { name: 'Alan Turing' }; // Rename `name` from `person` to `personName` after destructuring. const { name: personName } = person; console.log(name); // Jamie Corkhill <-- As expected. console.log(personName); // Alan Turing <-- Variable was renamed.

Schließlich können wir auch Standardwerte mit Umbenennung festlegen:

 const name = 'Jamie Corkhill'; const person = { location: 'New York City, United States' }; const { name: personName = 'Anonymous', location } = person; console.log(name); // Jamie Corkhill console.log(personName); // Anonymous console.log(location); // New York City, United States

Wie Sie sehen können, wird in diesem Fall name from person ( person.name ) in personName umbenannt und auf den Standardwert Anonymous gesetzt, falls nicht vorhanden.

Und natürlich kann dasselbe in Funktionssignaturen ausgeführt werden:

 const personOne = { name: 'User One', password: 'BCrypt Hash' }; const personTwo = { password: 'BCrypt Hash' }; const createUser = ({ name: personName = 'Anonymous', password }) => { if (!password) throw new Error('InvalidArgumentException'); console.log(personName); console.log(password); return { id: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15), name: personName, password: password // <-- We'll discuss this next. }; } createUser(personOne); // User One, BCrypt Hash createUser(personTwo); // Anonymous, BCrypt Hash

ES6-Objekt-Kurzschrift

Angenommen, Sie haben die folgende Fabrik: (Wir werden Fabriken später behandeln)

 const createPersonFactory = (name, location, position) => ({ name: name, location: location, position: position });

Man könnte diese Factory verwenden, um ein person wie folgt zu erstellen. Beachten Sie auch, dass die Factory implizit ein Objekt zurückgibt, was durch die Klammern um die Klammern der Pfeilfunktion ersichtlich ist.

 const person = createPersonFactory('Jamie', 'Texas', 'Developer'); console.log(person); // { ... }

Das kennen wir bereits aus der ES5 Object Literal Syntax. Beachten Sie jedoch, dass in der Factory-Funktion der Wert jeder Eigenschaft derselbe Name ist wie der Eigenschaftsbezeichner (Schlüssel) selbst. Das heißt — location: location oder name: name . Es stellte sich heraus, dass dies bei JS-Entwicklern ziemlich häufig vorkam.

Mit der abgekürzten Syntax von ES6 können wir das gleiche Ergebnis erzielen, indem wir die Factory wie folgt umschreiben:

 const createPersonFactory = (name, location, position) => ({ name, location, position }); const person = createPersonFactory('Jamie', 'Texas', 'Developer'); console.log(person);

Ausgabe erzeugen:

 { name: 'Jamie', location: 'Texas', position: 'Developer' }

Es ist wichtig zu wissen, dass wir diese Abkürzung nur verwenden können, wenn das Objekt, das wir erstellen möchten, dynamisch basierend auf Variablen erstellt wird, wobei die Variablennamen dieselben sind wie die Namen der Eigenschaften, denen die Variablen zugewiesen werden sollen.

Dieselbe Syntax funktioniert mit Objektwerten:

 const createPersonFactory = (name, location, position, extra) => ({ name, location, position, extra // <- right here. }); const extra = { interests: [ 'Mathematics', 'Quantum Mechanics', 'Spacecraft Launch Systems' ], favoriteLanguages: [ 'JavaScript', 'C#' ] }; const person = createPersonFactory('Jamie', 'Texas', 'Developer', extra); console.log(person);

Ausgabe erzeugen:

 { name: 'Jamie', location: 'Texas', position: 'Developer', extra: { interests: [ 'Mathematics', 'Quantum Mechanics', 'Spacecraft Launch Systems' ], favoriteLanguages: [ 'JavaScript', 'C#' ] } }

Als letztes Beispiel funktioniert dies auch mit Objektliteralen:

 const id = '314159265358979'; const name = 'Archimedes of Syracuse'; const location = 'Syracuse'; const greatMathematician = { id, name, location };

ES6 Spread-Operator (…)

Der Spread-Operator erlaubt uns, eine Vielzahl von Dingen zu tun, von denen wir einige hier besprechen werden.

Erstens können wir Eigenschaften von einem Objekt auf ein anderes Objekt verteilen:

 const myObjOne = { a: 'a', b: 'b' }; const myObjTwo = { ...myObjOne }:

Dies hat zur Folge, dass alle Eigenschaften von myObjOne auf myObjTwo werden, sodass myObjTwo jetzt { a: 'a', b: 'b' } . Wir können diese Methode verwenden, um vorherige Eigenschaften zu überschreiben. Angenommen, ein Benutzer möchte sein Konto aktualisieren:

 const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */ const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */ const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */ const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */

Das gleiche kann mit Arrays durchgeführt werden:

 const apollo13Astronauts = ['Jim', 'Jack', 'Fred']; const apollo11Astronauts = ['Neil', 'Buz', 'Michael']; const unionOfAstronauts = [...apollo13Astronauts, ...apollo11Astronauts]; console.log(unionOfAstronauts); // ['Jim', 'Jack', 'Fred', 'Neil', 'Buz, 'Michael'];

Beachten Sie hier, dass wir eine Vereinigung beider Sätze (Arrays) erstellt haben, indem wir die Arrays in einem neuen Array verteilt haben.

Der Rest/Spread-Operator hat noch viel mehr zu bieten, aber das geht über den Rahmen dieses Artikels hinaus. Es kann beispielsweise verwendet werden, um mehrere Argumente für eine Funktion zu erhalten. Wenn Sie mehr erfahren möchten, sehen Sie sich hier die MDN-Dokumentation an.

ES6 Asynchron/Warten

Async/Await ist eine Syntax, um den Schmerz der Verkettung von Versprechen zu lindern.

Das await reservierte Schlüsselwort ermöglicht es Ihnen, auf die Abwicklung eines Versprechens zu „warten“, aber es darf nur in Funktionen verwendet werden, die mit dem async Schlüsselwort gekennzeichnet sind. Angenommen, ich habe eine Funktion, die ein Versprechen zurückgibt. In einer neuen async Funktion kann ich auf das Ergebnis dieses Versprechens await , anstatt .then und .catch .

 // Returns a promise. const myFunctionThatReturnsAPromise = () => { return new Promise((resolve, reject) => { setTimeout(() => resolve('Hello'), 3000); }); } const myAsyncFunction = async () => { const promiseResolutionResult = await myFunctionThatReturnsAPromise(); console.log(promiseResolutionResult); }; // Writes the log statement after three seconds. myAsyncFunction();

Hier gibt es einiges zu beachten. Wenn wir await in einer async Funktion verwenden, geht nur der aufgelöste Wert in die Variable auf der linken Seite. Wenn die Funktion ablehnt, ist das ein Fehler, den wir abfangen müssen, wie wir gleich sehen werden. Darüber hinaus gibt jede als async gekennzeichnete Funktion standardmäßig ein Versprechen zurück.

Nehmen wir an, ich müsste zwei API-Aufrufe durchführen, einen mit der Antwort des ersteren. Mit Promises und Promise Chaining könntest du es so machen:

 const makeAPICall = route => new Promise((resolve, reject) => { console.log(route) resolve(route); }); const main = () => { makeAPICall('/whatever') .then(response => makeAPICall(response + ' second call')) .then(response => console.log(response + ' logged')) .catch(err => console.error(err)) }; main(); // Result: /* /whatever /whatever second call /whatever second call logged */

Was hier passiert, ist, dass wir zuerst makeAPICall und an /whatever übergeben, was beim ersten Mal protokolliert wird. Das Versprechen wird mit diesem Wert aufgelöst. Dann rufen wir erneut makeAPICall auf und übergeben ihm /whatever second call , der protokolliert wird, und wieder wird das Promise mit diesem neuen Wert aufgelöst. Schließlich nehmen wir den neuen Wert /whatever second call , mit dem das Promise gerade aufgelöst wurde, und protokollieren ihn selbst im letzten Protokoll, wobei wir am Ende ein logged anhängen. Wenn dies keinen Sinn macht, sollten Sie sich mit Promise Chaining befassen.

Mit async / await können wir Folgendes umgestalten:

 const main = async () => { const resultOne = await makeAPICall('/whatever'); const resultTwo = await makeAPICall(resultOne + ' second call'); console.log(resultTwo + ' logged'); };

Folgendes wird passieren. Die gesamte Funktion stoppt die Ausführung bei der allerersten await -Anweisung, bis das Versprechen des ersten Aufrufs von makeAPICall aufgelöst wird. Nach der Auflösung wird der aufgelöste Wert in resultOne platziert. Wenn dies geschieht, bewegt sich die Funktion zur zweiten await Anweisung und pausiert genau dort erneut für die Dauer der Promise-Abwicklung. Wenn das Promise aufgelöst wird, wird das Auflösungsergebnis in resultTwo . Wenn die Idee der Funktionsausführung blockiert klingt, fürchten Sie sich nicht, sie ist immer noch asynchron, und ich werde gleich erläutern, warum.

Dies zeigt nur den „glücklichen“ Weg. Für den Fall, dass eines der Promises abgelehnt wird, können wir das mit try/catch abfangen, denn wenn das Promise abgelehnt wird, wird ein Fehler geworfen – der der Fehler ist, mit dem das Promise abgelehnt wurde.

 const main = async () => { try { const resultOne = await makeAPICall('/whatever'); const resultTwo = await makeAPICall(resultOne + ' second call'); console.log(resultTwo + ' logged'); } catch (e) { console.log(e) } };

Wie ich bereits sagte, gibt jede als async deklarierte Funktion ein Versprechen zurück. Wenn Sie also eine asynchrone Funktion von einer anderen Funktion aufrufen möchten, können Sie normale Zusagen verwenden oder await , bis Sie die aufrufende Funktion async deklarieren. Wenn Sie jedoch eine async Funktion aus dem Code der obersten Ebene aufrufen und auf das Ergebnis warten möchten, müssen Sie .then und .catch .

Zum Beispiel:

 const returnNumberOne = async () => 1; returnNumberOne().then(value => console.log(value)); // 1

Oder Sie könnten einen Immediately Invoked Function Expression (IIFE) verwenden:

 (async () => { const value = await returnNumberOne(); console.log(value); // 1 })();

Wenn Sie await in einer async Funktion verwenden, wird die Ausführung der Funktion bei dieser await-Anweisung angehalten, bis das Promise erfüllt ist. Alle anderen Funktionen können jedoch frei mit der Ausführung fortfahren, sodass weder zusätzliche CPU-Ressourcen zugewiesen noch der Thread jemals blockiert wird. Ich sage das noch einmal – Operationen in dieser bestimmten Funktion zu diesem bestimmten Zeitpunkt werden angehalten, bis das Versprechen erfüllt ist, aber alle anderen Funktionen können frei ausgelöst werden. Stellen Sie sich einen HTTP-Webserver vor – auf Anfragebasis können alle Funktionen für alle Benutzer gleichzeitig ausgelöst werden, wenn Anfragen gestellt werden, es ist nur so, dass die async/await-Syntax die Illusion vermittelt, dass eine Operation synchron und blockierend ist verspricht einfacher zu arbeiten, aber auch hier bleibt alles nett und asynchron.

Dies ist nicht alles, was es zu async / await zu tun gibt, aber es sollte Ihnen helfen, die Grundprinzipien zu verstehen.

Klassische OOP-Fabriken

Wir verlassen jetzt die JavaScript -Welt und betreten die Java -Welt. Es kann eine Zeit kommen, in der der Erstellungsprozess eines Objekts (in diesem Fall eine Instanz einer Klasse – wieder Java) ziemlich komplex ist oder wenn wir verschiedene Objekte basierend auf einer Reihe von Parametern erzeugt haben möchten. Ein Beispiel könnte eine Funktion sein, die verschiedene Fehlerobjekte erstellt. Eine Fabrik ist ein gängiges Entwurfsmuster in der objektorientierten Programmierung und ist im Grunde eine Funktion, die Objekte erstellt. Um dies zu untersuchen, lassen Sie uns weg von JavaScript in die Welt von Java gehen. Dies wird für Entwickler sinnvoll sein, die aus einem klassischen OOP (dh nicht prototypischen), statisch typisierten Sprachhintergrund kommen. Wenn Sie kein solcher Entwickler sind, können Sie diesen Abschnitt gerne überspringen. Dies ist eine kleine Abweichung, und wenn das Befolgen hier Ihren JavaScript-Fluss unterbricht, dann überspringen Sie bitte auch diesen Abschnitt.

Das Fabrikmuster ist ein gängiges Erstellungsmuster und ermöglicht es uns, Objekte zu erstellen, ohne die erforderliche Geschäftslogik zur Durchführung dieser Erstellung offenzulegen.

Angenommen, wir schreiben ein Programm, das uns erlaubt, primitive Formen in n-Dimensionen zu visualisieren. Wenn wir beispielsweise einen Würfel bereitstellen, würden wir einen 2D-Würfel (ein Quadrat), einen 3D-Würfel (einen Würfel) und einen 4D-Würfel (ein Tesseract oder Hypercube) sehen. So könnte dies trivial und mit Ausnahme des eigentlichen Zeichenteils in Java geschehen.

 // Main.java // Defining an interface for the shape (can be used as a base type) interface IShape { void draw(); } // Implementing the interface for 2-dimensions: class TwoDimensions implements IShape { @Override public void draw() { System.out.println("Drawing a shape in 2D."); } } // Implementing the interface for 3-dimensions: class ThreeDimensions implements IShape { @Override public void draw() { System.out.println("Drawing a shape in 3D."); } } // Implementing the interface for 4-dimensions: class FourDimensions implements IShape { @Override public void draw() { System.out.println("Drawing a shape in 4D."); } } // Handles object creation class ShapeFactory { // Factory method (notice return type is the base interface) public IShape createShape(int dimensions) { switch(dimensions) { case 2: return new TwoDimensions(); case 3: return new ThreeDimensions(); case 4: return new FourDimensions(); default: throw new IllegalArgumentException("Invalid dimension."); } } } // Main class and entry point. public class Main { public static void main(String[] args) throws Exception { ShapeFactory shapeFactory = new ShapeFactory(); IShape fourDimensions = shapeFactory.createShape(4); fourDimensions.draw(); // Drawing a shape in 4D. } }

Wie Sie sehen können, definieren wir eine Schnittstelle, die eine Methode zum Zeichnen einer Form angibt. Indem die verschiedenen Klassen die Schnittstelle implementieren, können wir garantieren, dass alle Formen gezeichnet werden können (da sie alle eine überschreibbare draw gemäß der Schnittstellendefinition haben müssen). Da diese Form abhängig von den Dimensionen, in denen sie betrachtet wird, unterschiedlich gezeichnet wird, definieren wir Hilfsklassen, die die Schnittstelle implementieren, um die GPU-intensive Arbeit der Simulation des n-dimensionalen Renderings auszuführen. ShapeFactory übernimmt die Arbeit, die richtige Klasse zu instanziieren – die createShape Methode ist eine Factory, und wie die obige Definition ist sie eine Methode, die ein Objekt einer Klasse zurückgibt. Der Rückgabetyp von createShape ist die IShape Schnittstelle, weil die IShape -Schnittstelle der Basistyp aller Formen ist (weil sie eine draw Methode haben).

Dieses Java-Beispiel ist ziemlich trivial, aber Sie können leicht erkennen, wie nützlich es in größeren Anwendungen wird, in denen die Einrichtung zum Erstellen eines Objekts möglicherweise nicht so einfach ist. Ein Beispiel dafür wäre ein Videospiel. Angenommen, der Benutzer muss verschiedene Feinde überleben. Abstrakte Klassen und Schnittstellen können verwendet werden, um Kernfunktionen zu definieren, die allen Feinden zur Verfügung stehen (und Methoden, die außer Kraft gesetzt werden können), möglicherweise unter Verwendung des Delegationsmusters (bevorzugen Sie die Komposition gegenüber der Vererbung, wie die Gang of Four vorgeschlagen hat, damit Sie nicht in die Erweiterung von a einzelne Basisklasse und um das Testen/Mocking/DI einfacher zu machen). Für feindliche Objekte, die auf unterschiedliche Weise instanziiert werden, würde die Schnittstelle die Erstellung von Factory-Objekten ermöglichen, während sie sich auf den generischen Schnittstellentyp verlässt. Dies wäre sehr relevant, wenn der Feind dynamisch erstellt würde.

Ein weiteres Beispiel ist eine Builder-Funktion. Angenommen, wir verwenden das Delegationsmuster, um einen Klassendelegierten für andere Klassen arbeiten zu lassen, die eine Schnittstelle berücksichtigen. Wir könnten eine statische build -Methode in der Klasse platzieren, damit sie ihre eigene Instanz erstellt (vorausgesetzt, Sie verwenden keinen Dependency Injection Container/Framework). Anstatt jeden Setter anrufen zu müssen, können Sie Folgendes tun:

 public class User { private IMessagingService msgService; private String name; private int age; public User(String name, int age, IMessagingService msgService) { this.name = name; this.age = age; this.msgService = msgService; } public static User build(String name, int age) { return new User(name, age, new SomeMessageService()); } }

Ich werde das Delegationsmuster in einem späteren Artikel erklären, wenn Sie damit nicht vertraut sind – im Grunde erstellt es durch Komposition und in Bezug auf die Objektmodellierung eine „hat-ein“-Beziehung anstelle einer „ist-ein“-Beziehung. Beziehung, wie Sie es mit einer Erbschaft bekommen würden. Wenn Sie eine Mammal -Klasse und eine Dog -Klasse haben und Dog Mammal erweitert, dann ist ein Dog ein Mammal . Wenn Sie dagegen eine Bark -Klasse hatten und nur Instanzen von Bark an den Konstruktor von Dog übergeben haben, dann hat Dog -a Bark . Wie Sie sich vorstellen können, erleichtert dies insbesondere das Testen von Einheiten, da Sie Mocks einfügen und Fakten über das Mock behaupten können, solange Mock den Schnittstellenvertrag in der Testumgebung einhält.

Die obige static „Build“-Factory-Methode erstellt einfach ein neues Objekt von User und übergibt einen konkreten MessageService . Beachten Sie, wie dies aus der obigen Definition folgt – die Geschäftslogik zum Erstellen eines Objekts einer Klasse nicht verfügbar macht, oder in diesem Fall, die Erstellung des Messaging-Dienstes nicht dem Anrufer der Fabrik preiszugeben.

Auch dies ist nicht unbedingt die Art und Weise, wie Sie die Dinge in der realen Welt tun würden, aber es präsentiert die Idee einer Fabrikfunktion / -methode recht gut. Wir könnten stattdessen beispielsweise einen Dependency Injection-Container verwenden. Nun zurück zu JavaScript.

Beginnend mit Express

Express ist ein Webanwendungs-Framework für Node (verfügbar über ein NPM-Modul), das die Erstellung eines HTTP-Webservers ermöglicht. Es ist wichtig zu beachten, dass Express nicht das einzige Framework ist, das dies tut (es gibt Koa, Fastify usw.), und dass Node, wie im vorherigen Artikel gezeigt, ohne Express als eigenständige Einheit funktionieren kann. (Express ist lediglich ein Modul, das für Node entwickelt wurde – Node kann viele Dinge ohne es tun, obwohl Express für Webserver beliebt ist).

Lassen Sie mich noch einmal eine sehr wichtige Unterscheidung treffen. Zwischen Node/JavaScript und Express besteht eine Dichotomie. Node, die Laufzeit/Umgebung, in der Sie JavaScript ausführen, kann viele Dinge tun – Ihnen beispielsweise erlauben, React Native-Apps, Desktop-Apps, Befehlszeilentools usw. zu erstellen. Express ist nichts anderes als ein leichtes Framework, das Sie verwenden können Node/JS zum Erstellen von Webservern, anstatt sich mit dem Low-Level-Netzwerk und den HTTP-APIs von Node zu befassen. Sie benötigen Express nicht, um einen Webserver zu erstellen.

Bevor Sie mit diesem Abschnitt beginnen, empfehle ich Ihnen, den entsprechenden Abschnitt meines früheren Artikels zu lesen, der oben verlinkt ist, wenn Sie mit HTTP und HTTP-Anforderungen (GET, POST usw.) nicht vertraut sind.

Mit Express richten wir verschiedene Routen ein, an die HTTP-Anforderungen gestellt werden können, sowie die zugehörigen Endpunkte (bei denen es sich um Rückruffunktionen handelt), die ausgelöst werden, wenn eine Anforderung an diese Route gesendet wird. Machen Sie sich keine Sorgen, wenn Routen und Endpunkte derzeit keinen Sinn machen – ich werde sie später erklären.

Im Gegensatz zu anderen Artikeln werde ich den Ansatz verfolgen, den Quellcode Zeile für Zeile zu schreiben, anstatt die gesamte Codebasis in einen Ausschnitt zu packen und später zu erklären. Beginnen wir mit dem Öffnen eines Terminals (ich verwende Terminus auf Git Bash unter Windows – was eine nette Option für Windows-Benutzer ist, die eine Bash-Shell ohne Einrichtung des Linux-Subsystems wünschen), dem Einrichten der Boilerplate unseres Projekts und dem Öffnen in Visual Studio-Code.

 mkdir server && cd server touch server.js npm init -y npm install express code .

In der server.js -Datei beginne ich damit, express mit der require() Funktion anzufordern.

 const express = require('express');

require('express') weist Node an, das zuvor installierte Express-Modul zu holen, das sich derzeit im Ordner node_modules befindet (dafür tut npm install — erstellt einen Ordner node_modules und legt Module und ihre Abhängigkeiten dort ab). Gemäß der Konvention und wenn wir uns mit Express befassen, nennen wir die Variable, die das Rückgabeergebnis von require('express') enthält, express , obwohl sie beliebig heißen kann.

This returned result, which we have called express , is actually a function — a function we'll have to invoke to create our Express app and set up our routes. Again, by convention, we call this appapp being the return result of express() — that is, the return result of calling the function that has the name express as express() .

 const express = require('express'); const app = express(); // Note that the above variable names are the convention, but not required. // An example such as that below could also be used. const foo = require('express'); const bar = foo(); // Note also that the node module we installed is called express.

The line const app = express(); simply puts a new Express Application inside of the app variable. It calls a function named express (the return result of require('express') ) and stores its return result in a constant named app . If you come from an object-oriented programming background, consider this equivalent to instantiating a new object of a class, where app would be the object and where express() would call the constructor function of the express class. Remember, JavaScript allows us to store functions in variables — functions are first-class citizens. The express variable, then, is nothing more than a mere function. It's provided to us by the developers of Express.

I apologize in advance if I'm taking a very long time to discuss what is actually very basic, but the above, although primitive, confused me quite a lot when I was first learning back-end development with Node.

Inside the Express source code, which is open-source on GitHub, the variable we called express is a function entitled createApplication , which, when invoked, performs the work necessary to create an Express Application:

A snippet of Express source code:

 exports = module.exports = createApplication; /* * Create an express application */ // This is the function we are storing in the express variable. (- Jamie) function createApplication() { // This is what I mean by "Express App" (- Jamie) var app = function(req, res, next) { app.handle(req, res, next); }; mixin(app, EventEmitter.prototype, false); mixin(app, proto, false); // expose the prototype that will get set on requests app.request = Object.create(req, { app: { configurable: true, enumerable: true, writable: true, value: app } }) // expose the prototype that will get set on responses app.response = Object.create(res, { app: { configurable: true, enumerable: true, writable: true, value: app } }) app.init(); // See - `app` gets returned. (- Jamie) return app; }

GitHub: https://github.com/expressjs/express/blob/master/lib/express.js

With that short deviation complete, let's continue setting up Express. Thus far, we have required the module and set up our app variable.

 const express = require('express'); const app = express();

From here, we have to tell Express to listen on a port. Any HTTP Requests made to the URL and Port upon which our application is listening will be handled by Express. We do that by calling app.listen(...) , passing to it the port and a callback function which gets called when the server starts running:

 const PORT = 3000; app.listen(PORT, () => console.log(`Server is up on port {PORT}.`));

We notate the PORT variable in capital by convention, for it is a constant variable that will never change. You could do that with all variables that you declare const , but that would look messy. It's up to the developer or development team to decide on notation, so we'll use the above sparsely. I use const everywhere as a method of “defensive coding” — that is, if I know that a variable is never going to change then I might as well just declare it const . Since I define everything const , I make the distinction between what variables should remain the same on a per-request basis and what variables are true actual global constants.

Here is what we have thus far:

 const express = require('express'); const app = express(); const PORT = 3000; // We will build our API here. // ... // Binding our application to port 3000. app.listen(PORT, () => { console.log(`Server is up on port ${PORT}.`); });

Let's test this to see if the server starts running on port 3000.

I'll open a terminal and navigate to our project's root directory. I'll then run node server/server.js . Note that this assumes you have Node already installed on your system (You can check with node -v ).

If everything works, you should see the following in the terminal:

Server is up on port 3000.

Go ahead and hit Ctrl + C to bring the server back down.

If this doesn't work for you, or if you see an error such as EADDRINUSE , then it means you may have a service already running on port 3000. Pick another port number, like 3001, 3002, 5000, 8000, etc. Be aware, lower number ports are reserved and there is an upper bound of 65535.

At this point, it's worth taking another small deviation as to understand servers and ports in the context of computer networking. We'll return to Express in a moment. I take this approach, rather than introducing servers and ports first, for the purpose of relevance. That is, it is difficult to learn a concept if you fail to see its applicability. In this way, you are already aware of the use case for ports and servers with Express, so the learning experience will be more pleasurable.

A Brief Look At Servers And Ports

A server is simply a computer or computer program that provides some sort of “functionality” to the clients that talk to it. More generally, it's a device, usually connected to the Internet, that handles connections in a pre-defined manner. In our case, that “pre-defined manner” will be HTTP or the HyperText Transfer Protocol. Servers that use the HTTP Protocol are called Web Servers.

When building an application, the server is a critical component of the “client-server model”, for it permits the sharing and syncing of data (generally via databases or file systems) across devices. It's a cross-platform approach, in a way, for the SDKs of platforms against which you may want to code — be they web, mobile, or desktop — all provide methods (APIs) to interact with a server over HTTP or TCP/UDP Sockets. It's important to make a distinction here — by APIs, I mean programming language constructs to talk to a server, like XMLHttpRequest or the Fetch API in JavaScript, or HttpUrlConnection in Java, or even HttpClient in C#/.NET. This is different from the kind of REST API we'll be building in this article to perform CRUD Operations on a database.

To talk about ports, it's important to understand how clients connect to a server. A client requires the IP Address of the server and the Port Number of our specific service on that server. An IP Address, or Internet Protocol Address, is just an address that uniquely identifies a device on a network. Public and private IPs exist, with private addresses commonly used behind a router or Network Address Translator on a local network. You might see private IP Addresses of the form 192.168.XXX.XXX or 10.0.XXX.XXX . When articulating an IP Address, decimals are called “dots”. So 192.168.0.1 (a common router IP Addr.) might be pronounced, “one nine two dot one six eight dot zero dot one”. (By the way, if you're ever in a hotel and your phone/laptop won't direct you to the AP captive portal, try typing 192.168.0.1 or 192.168.1.1 or similar directly into Chrome).

For simplicity, and since this is not an article about the complexities of computer networking, assume that an IP Address is equivalent to a house address, allowing you to uniquely identify a house (where a house is analogous to a server, client, or network device) in a neighborhood. One neighborhood is one network. Put together all of the neighborhoods in the United States, and you have the public Internet. (This is a basic view, and there are many more complexities — firewalls, NATs, ISP Tiers (Tier One, Tier Two, and Tier Three), fiber optics and fiber optic backbones, packet switches, hops, hubs, etc., subnet masks, etc., to name just a few — in the real networking world.) The traceroute Unix command can provide more insight into the above, displaying the path (and associated latency) that packets take through a network as a series of “hops”.

Eine Portnummer identifiziert einen bestimmten Dienst, der auf einem Server ausgeführt wird. SSH oder Secure Shell, das den Remote-Shell-Zugriff auf ein Gerät ermöglicht, wird normalerweise auf Port 22 ausgeführt. FTP oder File Transfer Protocol (das beispielsweise mit einem FTP-Client verwendet werden kann, um statische Assets auf einen Server zu übertragen) wird normalerweise ausgeführt auf Port 21. Wir könnten also sagen, dass Häfen in unserer obigen Analogie spezifische Räume in jedem Haus sind, denn Räume in Häusern sind für verschiedene Dinge gemacht – ein Schlafzimmer zum Schlafen, eine Küche zum Zubereiten von Speisen, ein Esszimmer zum Verzehr des Gesagten Lebensmittel usw. entsprechen genau wie Häfen Programmen, die bestimmte Dienste ausführen. Für uns laufen Webserver normalerweise auf Port 80, obwohl Sie frei wählen können, welche Portnummer Sie wünschen, solange sie nicht von einem anderen Dienst verwendet werden (sie können nicht kollidieren).

Um auf eine Website zuzugreifen, benötigen Sie die IP-Adresse der Website. Trotzdem greifen wir normalerweise über eine URL auf Websites zu. Hinter den Kulissen wandelt ein DNS oder Domain Name Server diese URL in eine IP-Adresse um, sodass der Browser eine GET-Anfrage an den Server stellen , den HTML-Code abrufen und auf dem Bildschirm darstellen kann. 8.8.8.8 ist die Adresse eines der öffentlichen DNS-Server von Google. Sie können sich vorstellen, dass die Auflösung eines Hostnamens in eine IP-Adresse über einen Remote-DNS-Server einige Zeit in Anspruch nehmen wird, und Sie haben Recht. Um die Latenz zu reduzieren, verfügen Betriebssysteme über einen DNS-Cache – eine temporäre Datenbank, die DNS-Lookup-Informationen speichert, wodurch die Häufigkeit reduziert wird, mit der diese Lookups durchgeführt werden müssen. Der DNS-Resolver-Cache kann unter Windows mit dem CMD-Befehl ipconfig /displaydns und mit dem Befehl ipconfig /flushdns .

Auf einem Unix-Server erfordern häufigere Ports mit niedrigeren Nummern, wie 80, Berechtigungen auf Root- Ebene ( eskaliert, wenn Sie von einem Windows-Hintergrund kommen). Aus diesem Grund verwenden wir Port 3000 für unsere Entwicklungsarbeit, erlauben dem Server jedoch, die Portnummer (was auch immer verfügbar ist) auszuwählen, wenn wir sie in unserer Produktionsumgebung bereitstellen.

Beachten Sie schließlich, dass wir IP-Adressen direkt in die Suchleiste von Google Chrome eingeben können, wodurch der DNS-Auflösungsmechanismus umgangen wird. Wenn Sie beispielsweise 216.58.194.36 , gelangen Sie zu Google.com. Wenn wir in unserer Entwicklungsumgebung unseren eigenen Computer als Entwicklungsserver verwenden, verwenden wir localhost und Port 3000. Eine Adresse wird als hostname:port formatiert, sodass unser Server auf localhost:3000 läuft. Localhost oder 127.0.0.1 ist die Loopback-Adresse und bedeutet die Adresse von „diesem Computer“. Es ist ein Hostname und seine IPv4-Adresse wird zu 127.0.0.1 . Versuchen Sie jetzt, localhost auf Ihrem Computer zu pingen. Sie erhalten möglicherweise ::1 zurück – das ist die IPv6-Loopback-Adresse oder 127.0.0.1 zurück – das ist die IPv4-Loopback-Adresse. IPv4 und IPv6 sind zwei unterschiedliche IP-Adressformate, die unterschiedlichen Standards zugeordnet sind – einige IPv6-Adressen können in IPv4 konvertiert werden, aber nicht alle.

Zurück zu Express

Ich habe HTTP-Anforderungen, Verben und Statuscodes in meinem vorherigen Artikel Erste Schritte mit Node: Eine Einführung in APIs, HTTP und ES6+-JavaScript erwähnt. Wenn Sie das Protokoll nicht allgemein verstehen, können Sie gerne zum Abschnitt „HTTP- und HTTP-Anforderungen“ dieses Artikels springen.

Um ein Gefühl für Express zu bekommen, richten wir einfach unsere Endpunkte für die vier grundlegenden Operationen ein, die wir an der Datenbank durchführen werden – Erstellen, Lesen, Aktualisieren und Löschen, zusammenfassend als CRUD bekannt.

Denken Sie daran, dass wir über Routen in der URL auf Endpunkte zugreifen. Das heißt, obwohl die Wörter „Route“ und „Endpunkt“ häufig synonym verwendet werden, ist ein Endpunkt technisch gesehen eine Programmiersprachenfunktion (wie ES6 Arrow Functions), die einige serverseitige Vorgänge ausführt, während eine Route das ist, was sich hinter dem Endpunkt befindet von . Wir spezifizieren diese Endpunkte als Callback-Funktionen, die Express auslöst, wenn die entsprechende Anfrage vom Client an die Route gestellt wird, hinter der sich der Endpunkt befindet. Sie können sich das Obige merken, indem Sie erkennen, dass es Endpunkte sind, die eine Funktion ausführen, und die Route der Name ist, der für den Zugriff auf die Endpunkte verwendet wird. Wie wir sehen werden, kann dieselbe Route mehreren Endpunkten zugeordnet werden, indem verschiedene HTTP-Verben verwendet werden (ähnlich wie beim Überladen von Methoden, wenn Sie aus einem klassischen OOP-Hintergrund mit Polymorphismus kommen).

Denken Sie daran, dass wir der REST-Architektur (REpresentational State Transfer) folgen, indem wir Clients erlauben, Anfragen an unseren Server zu stellen. Dies ist schließlich eine REST- oder RESTful-API. Bestimmte Anfragen an bestimmte Routen lösen bestimmte Endpunkte aus, die bestimmte Dinge tun. Ein Beispiel für so etwas, das ein Endpunkt tun könnte, ist das Hinzufügen neuer Daten zu einer Datenbank, das Entfernen von Daten, das Aktualisieren von Daten usw.

Express weiß, welcher Endpunkt ausgelöst werden soll, weil wir ihm explizit die Anforderungsmethode (GET, POST usw.) und die Route mitteilen – wir definieren, welche Funktionen für bestimmte Kombinationen der oben genannten ausgelöst werden sollen, und der Client stellt die Anforderung, indem er a angibt Weg und Methode. Um es einfacher auszudrücken, mit Node sagen wir Express – „Hey, wenn jemand eine GET-Anfrage an diese Route stellt, dann feuere diese Funktion ab (verwende diesen Endpunkt)“. Die Dinge können komplizierter werden: „Express, wenn jemand eine GET-Anfrage an diese Route stellt, aber kein gültiges Authorization Bearer Token im Header seiner Anfrage hochsendet, dann antworte bitte mit einem HTTP 401 Unauthorized . Wenn sie ein gültiges Bearer Token besitzen, senden Sie bitte die geschützte Ressource, nach der sie gesucht haben, herunter, indem Sie den Endpunkt auslösen. Vielen Dank und einen schönen Tag.“ In der Tat wäre es schön, wenn Programmiersprachen auf diesem hohen Niveau sein könnten, ohne Mehrdeutigkeiten zu verlieren, aber es demonstriert dennoch die grundlegenden Konzepte.

Denken Sie daran, dass der Endpunkt in gewisser Weise hinter der Route lebt . Daher ist es zwingend erforderlich, dass der Client im Header der Anfrage angibt, welche Methode er verwenden möchte, damit Express herausfinden kann, was zu tun ist. Die Anforderung wird an eine bestimmte Route gesendet, die der Client (zusammen mit dem Anforderungstyp) beim Kontaktieren des Servers angibt, sodass Express das tun kann, was es tun muss, und wir das tun, was wir tun müssen, wenn Express unsere Rückrufe auslöst . Darauf kommt es an.

In den Codebeispielen zuvor haben wir die Funktion listen aufgerufen, die auf app verfügbar war, und ihr einen Port und einen Rückruf übergeben. app selbst ist, wenn Sie sich erinnern, das Rückgabeergebnis des Aufrufs der express Variablen als Funktion (d. h. express() ), und die express Variable ist das, was wir das Rückgabeergebnis genannt haben, weil wir 'express' aus unserem node_modules Ordner benötigen. Genauso wie listen in app aufgerufen wird, geben wir HTTP-Anforderungsendpunkte an, indem wir sie in app aufrufen. Schauen wir uns GET an:

 app.get('/my-test-route', () => { // ... });

Der erste Parameter ist ein string , und es ist die Route, hinter der der Endpunkt leben wird. Die Callback-Funktion ist der Endpunkt. Ich sage das noch einmal: Die Callback-Funktion – der zweite Parameter – ist der Endpunkt , der ausgelöst wird, wenn eine HTTP GET-Anfrage an die Route gestellt wird, die wir als erstes Argument angeben (in diesem Fall /my-test-route ).

Bevor wir nun weiter mit Express arbeiten, müssen wir wissen, wie Routen funktionieren. Die Route, die wir als Zeichenfolge angeben, wird aufgerufen, indem die Anfrage an www.domain.com/the-route-we-chose-earlier-as-a-string . In unserem Fall ist die Domäne localhost:3000 , was bedeutet, dass wir zum Auslösen der obigen Callback-Funktion eine GET-Anforderung an localhost:3000/my-test-route stellen müssen. Wenn wir oben eine andere Zeichenfolge als erstes Argument verwendet hätten, müsste die URL anders sein, damit sie mit der übereinstimmt, die wir in JavaScript angegeben haben.

Wenn Sie über solche Dinge sprechen, werden Sie wahrscheinlich von Glob Patterns hören. Wir könnten sagen, dass sich alle unsere API-Routen unter localhost:3000/** Glob Pattern befinden, wobei ** ein Platzhalter ist, der ein beliebiges Verzeichnis oder Unterverzeichnis bedeutet (beachten Sie, dass Routen keine Verzeichnisse sind), denen root ein übergeordnetes Element ist – das ist alles.

Lassen Sie uns fortfahren und dieser Callback-Funktion eine Protokollanweisung hinzufügen, sodass wir insgesamt Folgendes haben:

 // Getting the module from node_modules. const express = require('express'); // Creating our Express Application. const app = express(); // Defining the port we'll bind to. const PORT = 3000; // Defining a new endpoint behind the "/my-test-route" route. app.get('/my-test-route', () => { console.log('A GET Request was made to /my-test-route.'); }); // Binding the server to port 3000. app.listen(PORT, () => { console.log(`Server is up on port ${PORT}.`) });

Wir bringen unseren Server zum Laufen, indem wir node server/server.js (wobei Node auf unserem System installiert und global über Systemumgebungsvariablen zugänglich ist) im Stammverzeichnis des Projekts ausführen. Wie zuvor sollten Sie die Meldung sehen, dass der Server in der Konsole aktiv ist. Nachdem der Server ausgeführt wird, öffnen Sie einen Browser und besuchen Sie localhost:3000 in der URL-Leiste.

Sie sollten mit einer Fehlermeldung begrüßt werden, die besagt, dass Cannot GET / . Drücken Sie Strg + Umschalt + I unter Windows in Chrome, um die Entwicklerkonsole anzuzeigen. Dort sollten Sie sehen, dass wir einen 404 (Ressource nicht gefunden) haben. Das macht Sinn – wir haben dem Server nur gesagt, was er tun soll, wenn jemand localhost:3000/my-test-route besucht. Der Browser hat bei localhost:3000 nichts zu rendern (was localhost:3000/ mit einem Schrägstrich entspricht).

Wenn Sie sich das Terminalfenster ansehen, in dem der Server läuft, sollten keine neuen Daten vorhanden sein. Besuchen Sie jetzt localhost:3000/my-test-route in der URL-Leiste Ihres Browsers. Möglicherweise sehen Sie denselben Fehler in der Chrome-Konsole (weil der Browser den Inhalt zwischenspeichert und immer noch kein HTML zum Rendern hat), aber wenn Sie Ihr Terminal anzeigen, auf dem der Serverprozess ausgeführt wird, sehen Sie, dass die Rückruffunktion tatsächlich ausgelöst wurde und die Protokollnachricht wurde tatsächlich protokolliert.

Fahren Sie den Server mit Strg + C herunter.

Lassen Sie uns nun dem Browser etwas zum Rendern geben, wenn eine GET-Anfrage an diese Route gesendet wird, damit wir die Cannot GET / -Nachricht verlieren können. Ich nehme unser app.get() von früher und füge in der Callback-Funktion zwei Argumente hinzu. Denken Sie daran, dass die Callback-Funktion, die wir übergeben, hinter den Kulissen von Express aufgerufen wird und Express beliebige Argumente hinzufügen kann. Es fügt tatsächlich zwei hinzu (naja, technisch gesehen drei, aber das werden wir später sehen), und obwohl beide extrem wichtig sind, kümmern wir uns im Moment nicht um das erste. Das zweite Argument heißt res , kurz für response , und ich greife darauf zu, indem ich undefined als ersten Parameter setze:

 app.get('/my-test-route', (undefined, res) => { console.log('A GET Request was made to /my-test-route.'); });

Auch hier können wir das res -Argument beliebig nennen, aber res ist eine Konvention, wenn es um Express geht. res ist eigentlich ein Objekt, und darauf existieren verschiedene Methoden, um Daten an den Client zurückzusenden. In diesem Fall greife ich auf die auf res verfügbare Funktion send(...) zu, um HTML zurückzusenden, das der Browser rendert. Wir sind jedoch nicht auf das Zurücksenden von HTML beschränkt und können wählen, ob wir Text, ein JavaScript-Objekt, einen Stream (Streams sind besonders schön) oder was auch immer zurücksenden.

 app.get('/my-test-route', (undefined, res) => { console.log('A GET Request was made to /my-test-route.'); res.send('<h1>Hello, World!</h1>'); });

Wenn Sie den Server herunterfahren und dann wieder hochfahren und dann Ihren Browser auf der /my-test-route aktualisieren, sehen Sie, wie der HTML-Code gerendert wird.

Auf der Registerkarte "Netzwerk" der Chrome-Entwicklertools können Sie diese GET-Anforderung mit mehr Details anzeigen, da sie sich auf Header bezieht.

An diesem Punkt wird es uns guttun, etwas über Express Middleware zu lernen – Funktionen, die global ausgelöst werden können, nachdem ein Client eine Anfrage gestellt hat.

Express-Middleware

Express bietet Methoden zum Definieren benutzerdefinierter Middleware für Ihre Anwendung. Tatsächlich wird die Bedeutung von Express Middleware am besten in den Express-Dokumenten hier definiert)

Middleware -Funktionen sind Funktionen, die Zugriff auf das Anforderungsobjekt ( req ), das Antwortobjekt ( res ) und die nächste Middleware-Funktion im Anforderungs-Antwort-Zyklus der Anwendung haben. Die next-Middleware-Funktion wird üblicherweise durch eine Variable namens next bezeichnet.

Middleware-Funktionen können die folgenden Aufgaben ausführen:

  • Führen Sie einen beliebigen Code aus.
  • Nehmen Sie Änderungen an den Anforderungs- und Antwortobjekten vor.
  • Beenden Sie den Request-Response-Zyklus.
  • Rufen Sie die nächste Middleware-Funktion im Stack auf.

Mit anderen Worten, eine Middleware-Funktion ist eine benutzerdefinierte Funktion, die wir (der Entwickler) definieren können und die zwischen dem Empfang der Anfrage durch Express und dem Auslösen unserer entsprechenden Callback-Funktion als Vermittler fungiert. Wir könnten beispielsweise eine log erstellen, die jedes Mal protokolliert, wenn eine Anfrage gestellt wird. Beachten Sie, dass wir diese Middleware-Funktionen auch auslösen lassen können, nachdem unser Endpunkt ausgelöst wurde, je nachdem, wo Sie ihn im Stapel platzieren – etwas, das wir später sehen werden.

Um benutzerdefinierte Middleware zu spezifizieren, müssen wir sie als Funktion definieren und an app.use(...) .

 const myMiddleware = (req, res, next) => { console.log(`Middleware has fired at time ${Date().now}`); next(); } app.use(myMiddleware); // This is the app variable returned from express().

Alles zusammen haben wir jetzt:

 // Getting the module from node_modules. const express = require('express'); // Creating our Express Application. const app = express(); // Our middleware function. const myMiddleware = (req, res, next) => { console.log(`Middleware has fired at time ${Date().now}`); next(); } // Tell Express to use the middleware. app.use(myMiddleware); // Defining the port we'll bind to. const PORT = 3000; // Defining a new endpoint behind the "/my-test-route" route. app.get('/my-test-route', () => { console.log('A GET Request was made to /my-test-route.'); }); // Binding the server to port 3000. app.listen(PORT, () => { console.log(`Server is up on port ${PORT}.`) });

Wenn Sie die Anfragen erneut über den Browser stellen, sollten Sie jetzt sehen, dass Ihre Middleware-Funktion Zeitstempel auslöst und protokolliert. Um das Experimentieren zu fördern, versuchen Sie, den Aufruf der next Funktion zu entfernen, und sehen Sie, was passiert.

Die Middleware-Callback-Funktion wird mit drei Argumenten aufgerufen, req , res und next . req ist der Parameter, den wir beim Erstellen des GET-Handlers früher übersprungen haben, und es ist ein Objekt, das Informationen über die Anfrage enthält, wie Header, benutzerdefinierte Header, Parameter und jeden Körper, der möglicherweise vom Client gesendet wurde (wie z Sie tun dies mit einer POST-Anforderung). Ich weiß, dass wir hier über Middleware sprechen, aber sowohl die Endpunkte als auch die Middleware-Funktion werden mit req und res aufgerufen. req und res sind sowohl in der Middleware als auch im Endpunkt im Rahmen einer einzigen Anfrage vom Client gleich (es sei denn, das eine oder andere ändert es). Das heißt, Sie könnten beispielsweise eine Middleware-Funktion verwenden, um Daten zu bereinigen, indem Sie alle Zeichen entfernen, die möglicherweise darauf abzielen, SQL- oder NoSQL-Injektionen auszuführen, und dann die sichere req an den Endpunkt übergeben.

res ermöglicht es Ihnen, wie bereits erwähnt, auf verschiedene Arten, Daten an den Client zurückzusenden.

next ist eine Callback-Funktion, die Sie ausführen müssen, wenn die Middleware ihre Arbeit beendet hat, um die nächste Middleware-Funktion im Stack oder am Endpunkt aufzurufen. Beachten Sie unbedingt, dass Sie dies im then -Block aller asynchronen Funktionen aufrufen müssen, die Sie in der Middleware auslösen. Abhängig von Ihrem asynchronen Vorgang möchten Sie ihn möglicherweise im catch -Block aufrufen oder nicht. Das heißt, die myMiddleware Funktion wird ausgelöst, nachdem die Anforderung vom Client gestellt wurde, aber bevor die Endpunktfunktion der Anforderung ausgelöst wird. Wenn wir diesen Code ausführen und eine Anfrage stellen, sollte in der Konsole die Meldung Middleware has fired... vor der Meldung A GET Request was made to... angezeigt werden. Wenn Sie next() nicht aufrufen, wird der letzte Teil nie ausgeführt – Ihre Endpunktfunktion für die Anfrage wird nicht ausgelöst.

Beachten Sie auch, dass ich diese Funktion als solche auch anonym hätte definieren können (eine Konvention, an die ich mich halten werde):

 app.use((req, res, next) => { console.log(`Middleware has fired at time ${Date().now}`); next(); });

Für alle, die neu in JavaScript und ES6 sind, sollte das folgende Beispiel helfen, wenn die Art und Weise, wie das obige funktioniert, nicht sofort sinnvoll ist. Wir definieren einfach eine Callback-Funktion (die anonyme Funktion), die eine andere Callback-Funktion ( next ) als Argument akzeptiert. Wir nennen eine Funktion, die ein Funktionsargument übernimmt, eine Funktion höherer Ordnung. Betrachten Sie es auf die folgende Weise – es zeigt ein grundlegendes Beispiel dafür, wie der Express-Quellcode hinter den Kulissen funktionieren könnte:

 console.log('Suppose a request has just been made from the client.\n'); // This is what (it's not exactly) the code behind app.use() might look like. const use = callback => { // Simple log statement to see where we are. console.log('Inside use() - the "use" function has been called.'); // This depicts the termination of the middleware. const next = () => console.log('Terminating Middleware!\n'); // Suppose req and res are defined above (Express provides them). const req = res = null; // "callback" is the "middleware" function that is passed into "use". // "next" is the above function that pretends to stop the middleware. callback(req, res, next); }; // This is analogous to the middleware function we defined earlier. // It gets passed in as "callback" in the "use" function above. const myMiddleware = (req, res, next) => { console.log('Inside the myMiddleware function!'); next(); } // Here, we are actually calling "use()" to see everything work. use(myMiddleware); console.log('Moving on to actually handle the HTTP Request or the next middleware function.');

Wir rufen zuerst use , was myMiddleware als Argument verwendet. myMiddleware ist an und für sich eine Funktion, die drei Argumente akzeptiert – req , res und next . Innerhalb von use wird myMiddlware aufgerufen, und diese drei Argumente werden übergeben. Als next folgt eine in use definierte Funktion. myMiddleware ist als callback in der use -Methode definiert. Wenn ich use in diesem Beispiel auf ein Objekt namens app gesetzt hätte, hätten wir das Setup von Express vollständig nachahmen können, wenn auch ohne Sockets oder Netzwerkkonnektivität.

In diesem Fall sind sowohl myMiddleware als auch callback Funktionen höherer Ordnung, da sie beide Funktionen als Argumente annehmen.

Wenn Sie diesen Code ausführen, sehen Sie die folgende Antwort:

 Suppose a request has just been made from the client. Inside use() - the "use" function has been called. Inside the middleware function! Terminating Middleware! Moving on to actually handle the HTTP Request or the next middleware function.

Beachten Sie, dass ich auch anonyme Funktionen hätte verwenden können, um das gleiche Ergebnis zu erzielen:

 console.log('Suppose a request has just been made from the client.'); // This is what (it's not exactly) the code behind app.use() might look like. const use = callback => { // Simple log statement to see where we are. console.log('Inside use() - the "use" function has been called.'); // This depicts the termination of the middlewear. const next = () => console.log('Terminating Middlewear!'); // Suppose req and res are defined above (Express provides them). const req = res = null; // "callback" is the function which is passed into "use". // "next" is the above function that pretends to stop the middlewear. callback(req, res, () => { console.log('Terminating Middlewear!'); }); }; // Here, we are actually calling "use()" to see everything work. use((req, res, next) => { console.log('Inside the middlewear function!'); next(); }); console.log('Moving on to actually handle the HTTP Request.');

Nachdem das hoffentlich geklärt ist, können wir uns nun der eigentlichen Aufgabe zuwenden – dem Einrichten unserer Middleware.

Tatsache ist, dass Sie Daten normalerweise über eine HTTP-Anforderung senden müssen. Dazu haben Sie einige verschiedene Möglichkeiten – Senden von URL-Abfrageparametern, Senden von Daten, auf die über das req -Objekt zugegriffen werden kann, von dem wir zuvor erfahren haben, usw. Dieses Objekt ist nicht nur im Callback zum Aufrufen von app.use() , sondern auch an jeden beliebigen Endpunkt. Wir haben zuvor undefined als Füllelement verwendet, damit wir uns auf res konzentrieren konnten, um HTML an den Client zurückzusenden, aber jetzt brauchen wir Zugriff darauf.

 app.use('/my-test-route', (req, res) => { // The req object contains client-defined data that is sent up. // The res object allows the server to send data back down. });

HTTP-POST-Anforderungen erfordern möglicherweise , dass wir ein Body-Objekt an den Server senden. Wenn Sie ein Formular auf dem Client haben und den Namen und die E-Mail-Adresse des Benutzers verwenden, werden Sie diese Daten wahrscheinlich im Text der Anfrage an den Server senden.

Schauen wir uns an, wie das auf der Client-Seite aussehen könnte:

 <!DOCTYPE html> <html> <body> <form action="https://localhost:3000/email-list" method="POST" > <input type="text" name="nameInput"> <input type="email" name="emailInput"> <input type="submit"> </form> </body> </html>

Auf der Serverseite:

 app.post('/email-list', (req, res) => { // What do we now? // How do we access the values for the user's name and email? });

Um auf den Namen und die E-Mail-Adresse des Benutzers zuzugreifen, müssen wir eine bestimmte Art von Middleware verwenden. Dadurch werden die Daten in einem Objekt namens body available on req . Body Parser war eine beliebte Methode, dies zu tun, die von den Express-Entwicklern als eigenständiges NPM-Modul verfügbar war. Jetzt wird Express mit seiner eigenen Middleware vorinstalliert, um dies zu tun, und wir nennen es so:

 app.use(express.urlencoded({ extended: true }));

Jetzt können wir tun:

 app.post('/email-list', (req, res) => { console.log('User Name: ', req.body.nameInput); console.log('User Email: ', req.body.emailInput); });

Dies nimmt lediglich alle benutzerdefinierten Eingaben entgegen, die vom Client gesendet werden, und stellt sie im body -Objekt von req zur Verfügung. Beachten Sie, dass wir bei req.body jetzt nameInput und emailInput haben, die die Namen der input Tags im HTML-Code sind. Nun, diese clientdefinierten Daten sollten als gefährlich betrachtet werden (vertraue niemals dem Client) und müssen bereinigt werden, aber dazu kommen wir später.

Eine andere Art von Middleware, die von express bereitgestellt wird, ist express.json() . express.json wird verwendet, um alle JSON-Nutzlasten zu verpacken, die in einer Anfrage vom Client an req.body werden, während express.urlencoded alle eingehenden Anfragen mit Zeichenfolgen, Arrays oder anderen URL-codierten Daten an req.body . Kurz gesagt, beide manipulieren req.body , aber .json() ist für JSON-Payloads und .urlencoded() ist unter anderem für POST-Abfrageparameter.

Anders gesagt, eingehende Anfragen mit einem Content-Type: application/json -Header (z. B. die Angabe eines POST-Bodys mit der fetch -API) werden von express.json() verarbeitet, während Anfragen mit dem Header Content-Type: application/x-www-form-urlencoded (wie HTML-Formulare) wird mit express.urlencoded() behandelt. Das macht jetzt hoffentlich Sinn.

Starten unserer CRUD-Routen für MongoDB

Hinweis : Bei der Durchführung von PATCH-Anforderungen in diesem Artikel halten wir uns nicht an die JSONPatch-RFC-Spezifikation – ein Problem, das wir im nächsten Artikel dieser Serie beheben werden.

In Anbetracht dessen, dass wir verstehen, dass wir jeden Endpunkt spezifizieren, indem wir die relevante Funktion auf app aufrufen, ihr die Route und eine Callback-Funktion übergeben, die die Anfrage- und Antwortobjekte enthält, können wir damit beginnen, unsere CRUD-Routen für die Bookshelf-API zu definieren. In Anbetracht der Tatsache, dass dies ein einführender Artikel ist, werde ich mich weder darum kümmern, die HTTP- und REST-Spezifikationen vollständig zu befolgen, noch werde ich versuchen, die sauberste mögliche Architektur zu verwenden. Das kommt in einem zukünftigen Artikel.

Ich öffne die server.js -Datei, die wir bisher verwendet haben, und entleere alles, um mit der folgenden sauberen Tafel zu beginnen:

 // Getting the module from node_modules. const express = require('express'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true )); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // ... // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

Betrachten Sie den gesamten folgenden Code, um den Teil // ... der obigen Datei aufzunehmen.

Um unsere Endpunkte zu definieren, und weil wir eine REST-API erstellen, sollten wir die richtige Methode zum Benennen von Routen besprechen. Auch hier sollten Sie sich den HTTP-Abschnitt meines früheren Artikels ansehen, um weitere Informationen zu erhalten. Wir haben es mit Büchern zu tun, daher befinden sich alle Routen hinter /books (die Plural-Namenskonvention ist Standard).

Anfrage Route
POST /books
BEKOMMEN /books/id
PATCH /books/id
LÖSCHEN /books/id

Wie Sie sehen, muss beim POSTen eines Buchs keine ID angegeben werden, da wir (oder vielmehr MongoDB) sie serverseitig automatisch für uns generieren werden. Das Abrufen, PATCHen und Löschen von Büchern erfordert alle, dass wir diese ID an unseren Endpunkt übergeben, was wir später besprechen werden. Lassen Sie uns zunächst einfach die Endpunkte erstellen:

 // HTTP POST /books app.post('/books', (req, res) => { // ... console.log('A POST Request was made!'); }); // HTTP GET /books/:id app.get('/books/:id', (req, res) => { // ... console.log(`A GET Request was made! Getting book ${req.params.id}`); }); // HTTP PATCH /books/:id app.patch('/books/:id', (req, res) => { // ... console.log(`A PATCH Request was made! Updating book ${req.params.id}`); }); // HTTP DELETE /books/:id app.delete('/books/:id', (req, res) => { // ... console.log(`A DELETE Request was made! Deleting book ${req.params.id}`); });

Die :id Syntax teilt Express mit, dass id ein dynamischer Parameter ist, der in der URL weitergegeben wird. Wir haben Zugriff darauf über das params Objekt, das auf req verfügbar ist. Ich weiß, dass „wir haben auf req Zugriff darauf“ klingt wie Zauberei, und Zauberei (die es nicht gibt) ist beim Programmieren gefährlich, aber Sie müssen bedenken, dass Express keine Blackbox ist. Es ist ein Open-Source-Projekt, das auf GitHub unter einer MIT-Lizenz verfügbar ist. Sie können den Quellcode einfach anzeigen, wenn Sie sehen möchten, wie dynamische Abfrageparameter in das req -Objekt eingefügt werden.

Alles in allem haben wir jetzt Folgendes in unserer server.js -Datei:

 // Getting the module from node_modules. const express = require('express'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // HTTP POST /books app.post('/books', (req, res) => { // ... console.log('A POST Request was made!'); }); // HTTP GET /books/:id app.get('/books/:id', (req, res) => { // ... console.log(`A GET Request was made! Getting book ${req.params.id}`); }); // HTTP PATCH /books/:id app.patch('/books/:id', (req, res) => { // ... console.log(`A PATCH Request was made! Updating book ${req.params.id}`); }); // HTTP DELETE /books/:id app.delete('/books/:id', (req, res) => { // ... console.log(`A DELETE Request was made! Deleting book ${req.params.id}`); }); // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

Fahren Sie fort und starten Sie den Server, führen node server.js über das Terminal oder die Befehlszeile aus und besuchen Sie Ihren Browser. Öffnen Sie die Chrome-Entwicklungskonsole und besuchen Sie in der URL-Leiste (Uniform Resource Locator) localhost:3000/books . Sie sollten bereits die Anzeige im Terminal Ihres Betriebssystems sehen, dass der Server aktiv ist, sowie die Protokollanweisung für GET.

Bisher haben wir einen Webbrowser verwendet, um GET-Anforderungen auszuführen. Das ist gut für den Anfang, aber wir werden schnell feststellen, dass es bessere Tools zum Testen von API-Routen gibt. Tatsächlich könnten wir fetch direkt in die Konsole einfügen oder einen Onlinedienst verwenden. In unserem Fall und um Zeit zu sparen, verwenden wir cURL und Postman. Ich verwende beide in diesem Artikel (obwohl Sie entweder oder verwenden könnten), damit ich sie vorstellen kann, falls Sie sie noch nicht verwendet haben. cURL ist eine Bibliothek (eine sehr, sehr wichtige Bibliothek) und ein Befehlszeilentool, das entwickelt wurde, um Daten mit verschiedenen Protokollen zu übertragen. Postman ist ein GUI-basiertes Tool zum Testen von APIs. Nachdem Sie die entsprechenden Installationsanweisungen für beide Tools auf Ihrem Betriebssystem befolgt haben, stellen Sie sicher, dass Ihr Server noch läuft, und führen Sie dann die folgenden Befehle (nacheinander) in einem neuen Terminal aus. Es ist wichtig, dass Sie sie einzeln eingeben und ausführen und dann die Protokollnachricht im separaten Terminal Ihres Servers ansehen. Beachten Sie außerdem, dass das Kommentarsymbol der Standardprogrammiersprache // kein gültiges Symbol in Bash oder MS-DOS ist. Sie müssen diese Zeilen weglassen, und ich verwende sie hier nur, um jeden Block von cURL -Befehlen zu beschreiben.

 // HTTP POST Request (Localhost, IPv4, IPv6) curl -X POST https://localhost:3000/books curl -X POST https://127.0.0.1:3000/books curl -X POST https://[::1]:3000/books // HTTP GET Request (Localhost, IPv4, IPv6) curl -X GET https://localhost:3000/books/123abc curl -X GET https://127.0.0.1:3000/books/book-id-123 curl -X GET https://[::1]:3000/books/book-abc123 // HTTP PATCH Request (Localhost, IPv4, IPv6) curl -X PATCH https://localhost:3000/books/456 curl -X PATCH https://127.0.0.1:3000/books/218 curl -X PATCH https://[::1]:3000/books/some-id // HTTP DELETE Request (Localhost, IPv4, IPv6) curl -X DELETE https://localhost:3000/books/abc curl -X DELETE https://127.0.0.1:3000/books/314 curl -X DELETE https://[::1]:3000/books/217

Wie Sie sehen können, kann die als URL-Parameter übergebene ID ein beliebiger Wert sein. Das Flag -X gibt den Typ der HTTP-Anfrage an (kann bei GET weggelassen werden), und wir geben die URL an, an die die Anfrage danach gestellt wird. Ich habe jede Anfrage dreimal dupliziert, sodass Sie sehen können, dass alles immer noch funktioniert, unabhängig davon, ob Sie den localhost Hostnamen, die IPv4-Adresse ( 127.0.0.1 ), in die localhost aufgelöst wird, oder die IPv6-Adresse ( ::1 ), in die localhost aufgelöst wird, verwenden . Beachten Sie, dass cURL IPv6-Adressen in eckige Klammern setzen muss.

Wir sind jetzt an einem anständigen Ort – wir haben die einfache Struktur unserer Routen und Endpunkte eingerichtet. Der Server läuft korrekt und akzeptiert HTTP-Anforderungen so, wie wir es erwarten. Im Gegensatz zu dem, was Sie vielleicht erwarten, ist es an dieser Stelle nicht mehr weit – wir müssen nur unsere Datenbank einrichten, sie hosten (mithilfe eines Database-as-a-Service – MongoDB Atlas) und Daten darauf speichern (und Validierung durchführen und Fehlerantworten erstellen).

Einrichten einer Produktions-MongoDB-Datenbank

Um eine Produktionsdatenbank einzurichten, gehen wir zur MongoDB Atlas-Startseite und melden uns für ein kostenloses Konto an. Erstellen Sie danach einen neuen Cluster. Sie können die Standardeinstellungen beibehalten und eine für die Gebührenstufe geltende Region auswählen. Klicken Sie dann auf die Schaltfläche „Cluster erstellen“. Das Erstellen des Clusters dauert einige Zeit, und dann können Sie Ihre Datenbank-URL und Ihr Kennwort abrufen. Beachten Sie diese, wenn Sie sie sehen. Wir werden sie vorerst fest codieren und später aus Sicherheitsgründen in Umgebungsvariablen speichern. Wenn Sie Hilfe beim Erstellen und Herstellen einer Verbindung zu einem Cluster benötigen, verweise ich Sie auf die MongoDB-Dokumentation, insbesondere auf diese Seite und diese Seite, oder Sie können unten einen Kommentar hinterlassen, und ich werde versuchen, Ihnen zu helfen.

Erstellen eines Mongoose-Modells

Es wird empfohlen, dass Sie die Bedeutung von Dokumenten und Sammlungen im Kontext von NoSQL (Not Only SQL – Structured Query Language) verstehen. Als Referenz können Sie sowohl die Mongoose-Schnellstartanleitung als auch den MongoDB-Abschnitt meines früheren Artikels lesen.

Wir haben jetzt eine Datenbank, die bereit ist, CRUD-Operationen zu akzeptieren. Mongoose ist ein Node-Modul (oder ODM – Object Document Mapper), mit dem wir diese Operationen ausführen (einige der Komplexitäten abstrahieren) und das Schema oder die Struktur der Datenbanksammlung einrichten können.

Als wichtiger Haftungsausschluss gibt es viele Kontroversen um ORMs und solche Muster wie Active Record oder Data Mapper. Einige Entwickler schwören auf ORMs und andere dagegen (in der Annahme, dass sie im Weg stehen). Es ist auch wichtig zu beachten, dass ORMs viel abstrahieren, wie Verbindungspooling, Socket-Verbindungen und Handhabung usw. Sie könnten problemlos den MongoDB Native Driver (ein weiteres NPM-Modul) verwenden, aber es würde viel mehr Arbeit bedeuten. Obwohl empfohlen wird, dass Sie mit dem nativen Treiber spielen, bevor Sie ORMs verwenden, lasse ich den nativen Treiber hier der Kürze halber aus. Für komplexe SQL-Operationen in einer relationalen Datenbank sind nicht alle ORMs für die Abfragegeschwindigkeit optimiert, und Sie schreiben möglicherweise Ihr eigenes Roh-SQL. ORMs can come into play a lot with Domain-Driven Design and CQRS, among others. They are an established concept in the .NET world, and the Node.js community has not completely caught up yet — TypeORM is better, but it's not NHibernate or Entity Framework.

To create our Model, I'll create a new folder in the server directory entitled models , within which I'll create a single file with the name book.js . Thus far, our project's directory structure is as follows:

 - server - node_modules - models - book.js - package.json - server.js

Indeed, this directory structure is not required, but I use it here because it's simple. Allow me to note that this is not at all the kind of architecture you want to use for larger applications (and you might not even want to use JavaScript — TypeScript could be a better option), which I discuss in this article's closing. The next step will be to install mongoose , which is performed via, as you might expect, npm i mongoose .

The meaning of a Model is best ascertained from the Mongoose documentation:

Models are fancy constructors compiled from Schema definitions. An instance of a model is called a document. Models are responsible for creating and reading documents from the underlying MongoDB database.

Before creating the Model, we'll define its Schema. A Schema will, among others, make certain expectations about the value of the properties provided. MongoDB is schemaless, and thus this functionality is provided by the Mongoose ODM. Let's start with a simple example. Suppose I want my database to store a user's name, email address, and password. Traditionally, as a plain old JavaScript Object (POJO), such a structure might look like this:

 const userDocument = { name: 'Jamie Corkhill', email: '[email protected]', password: 'Bcrypt Hash' };

If that above object was how we expected our user's object to look, then we would need to define a schema for it, like this:

 const schema = { name: { type: String, trim: true, required: true }, email: { type: String, trim: true, required: true }, password: { type: String, required: true } };

Notice that when creating our schema, we define what properties will be available on each document in the collection as an object in the schema. In our case, that's name , email , and password . The fields type , trim , required tell Mongoose what data to expect. If we try to set the name field to a number, for example, or if we don't provide a field, Mongoose will throw an error (because we are expecting a type of String ), and we can send back a 400 Bad Request to the client. This might not make sense right now because we have defined an arbitrary schema object. However, the fields of type , trim , and required (among others) are special validators that Mongoose understands. trim , for example, will remove any whitespace from the beginning and end of the string. We'll pass the above schema to mongoose.Schema() in the future and that function will know what to do with the validators.

Understanding how Schemas work, we'll create the model for our Books Collection of the Bookshelf API. Let's define what data we require:

  1. Titel

  2. ISBN Number

  3. Autor

    1. Vorname

    2. Familienname, Nachname

  4. Publishing Date

  5. Finished Reading (Boolean)

I'm going to create this in the book.js file we created earlier in /models . Like the example above, we'll be performing validation:

 const mongoose = require('mongoose'); // Define the schema: const mySchema = { title: { type: String, required: true, trim: true, }, isbn: { type: String, required: true, trim: true, }, author: { firstName:{ type: String, required: true, trim: true }, lastName: { type: String, required: true, trim: true } }, publishingDate: { type: String }, finishedReading: { type: Boolean, required: true, default: false } }

default will set a default value for the property if none is provided — finishedReading for example, although a required field, will be set automatically to false if the client does not send one up.

Mongoose also provides the ability to perform custom validation on our fields, which is done by supplying the validate() method, which attains the value that was attempted to be set as its one and only parameter. In this function, we can throw an error if the validation fails. Hier ist ein Beispiel:

 // ... isbn: { type: String, required: true, trim: true, validate(value) { if (!validator.isISBN(value)) { throw new Error('ISBN is invalid.'); } } } // ...

Now, if anyone supplies an invalid ISBN to our model, Mongoose will throw an error when trying to save that document to the collection. I've already installed the NPM module validator via npm i validator and required it. validator contains a bunch of helper functions for common validation requirements, and I use it here instead of RegEx because ISBNs can't be validated with RegEx alone due to a tailing checksum. Remember, users will be sending a JSON body to one of our POST routes. That endpoint will catch any errors (such as an invalid ISBN) when attempting to save, and if one is thrown, it'll return a blank response with an HTTP 400 Bad Request status — we haven't yet added that functionality.

Finally, we have to define our schema of earlier as the schema for our model, so I'll make a call to mongoose.Schema() passing in that schema:

 const bookSchema = mongoose.Schema(mySchema);

To make things more precise and clean, I'll replace the mySchema variable with the actual object all on one line:

 const bookSchema = mongoose.Schema({ title:{ type: String, required: true, trim: true, }, isbn:{ type: String, required: true, trim: true, validate(value) { if (!validator.isISBN(value)) { throw new Error('ISBN is invalid.'); } } }, author:{ firstName: { type: String required: true, trim: true }, lastName:{ type: String, required: true, trim: true } }, publishingDate:{ type: String }, finishedReading:{ type: Boolean, required: true, default: false } });

Let's take a final moment to discuss this schema. We are saying that each of our documents will consist of a title, an ISBN, an author with a first and last name, a publishing date, and a finishedReading boolean.

  1. title will be of type String , it's a required field, and we'll trim any whitespace.
  2. isbn will be of type String , it's a required field, it must match the validator, and we'll trim any whitespace.
  3. author is of type object containing a required, trimmed, string firstName and a required, trimmed, string lastName.
  4. publishingDate is of type String (although we could make it of type Date or Number for a Unix timestamp.
  5. finishedReading is a required boolean that will default to false if not provided.

With our bookSchema defined, Mongoose knows what data and what fields to expect within each document to the collection that stores books. However, how do we tell it what collection that specific schema defines? We could have hundreds of collections, so how do we correlate, or tie, bookSchema to the Book collection?

The answer, as seen earlier, is with the use of models. We'll use bookSchema to create a model, and that model will model the data to be stored in the Book collection, which will be created by Mongoose automatically.

Append the following lines to the end of the file:

 const Book = mongoose.model('Book', bookSchema); module.exports = Book;

As you can see, we have created a model, the name of which is Book (— the first parameter to mongoose.model() ), and also provided the ruleset, or schema, to which all data is saved in the Book collection will have to abide. We export this model as a default export, allowing us to require the file for our endpoints to access. Book is the object upon which we'll call all of the required functions to Create, Read, Update, and Delete data which are provided by Mongoose.

Altogether, our book.js file should look as follows:

 const mongoose = require('mongoose'); const validator = require('validator'); // Define the schema. const bookSchema = mongoose.Schema({ title:{ type: String, required: true, trim: true, }, isbn:{ type: String, required: true, trim: true, validate(value) { if (!validator.isISBN(value)) { throw new Error('ISBN is invalid.'); } } }, author:{ firstName: { type: String, required: true, trim: true }, lastName:{ type: String, required: true, trim: true } }, publishingDate:{ type: String }, finishedReading:{ type: Boolean, required: true, default: false } }); // Create the "Book" model of name Book with schema bookSchema. const Book = mongoose.model('Book', bookSchema); // Provide the model as a default export. module.exports = Book;

Connecting To MongoDB (Basics)

Don't worry about copying down this code. I'll provide a better version in the next section. To connect to our database, we'll have to provide the database URL and password. We'll call the connect method available on mongoose to do so, passing to it the required data. For now, we are going hardcode the URL and password — an extremely frowned upon technique for many reasons: namely the accidental committing of sensitive data to a public (or private made public) GitHub Repository. Realize also that commit history is saved, and that if you accidentally commit a piece of sensitive data, removing it in a future commit will not prevent people from seeing it (or bots from harvesting it), because it's still available in the commit history. CLI tools exist to mitigate this issue and remove history.

As stated, for now, we'll hard code the URL and password, and then save them to environment variables later. At this point, let's look at simply how to do this, and then I'll mention a way to optimize it.

const mongoose = require('mongoose'); const MONGODB_URL = 'Your MongoDB URL'; mongoose.connect(MONGODB_URL, { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false, useUnifiedTopology: true });

Dadurch wird eine Verbindung zur Datenbank hergestellt. Wir geben die URL an, die wir aus dem MongoDB-Atlas-Dashboard erhalten haben, und das als zweiter Parameter übergebene Objekt gibt Funktionen an, die verwendet werden sollen, um unter anderem Verfallswarnungen zu verhindern.

Mongoose, das hinter den Kulissen den zentralen MongoDB Native Driver verwendet, muss versuchen, mit wichtigen Änderungen am Treiber Schritt zu halten. In einer neuen Version des Treibers wurde der Mechanismus zum Analysieren von Verbindungs-URLs geändert, sodass wir das useNewUrlParser: true , um anzugeben, dass wir die neueste verfügbare Version des offiziellen Treibers verwenden möchten.

Wenn Sie Indizes (und sie werden „Indizes“ und nicht „Indizes“ genannt) (auf die wir in diesem Artikel nicht eingehen) für Daten in Ihrer Datenbank festlegen, verwendet Mongoose standardmäßig die im nativen Treiber verfügbare ensureIndex() Funktion. MongoDB hat diese Funktion zugunsten von createIndex() , und wenn Sie das Flag useCreateIndex auf true setzen, wird Mongoose angewiesen, die createIndex() -Methode des Treibers zu verwenden, bei der es sich um die nicht veraltete Funktion handelt.

Mongooses ursprüngliche Version von findOneAndUpdate (eine Methode, um ein Dokument in einer Datenbank zu finden und zu aktualisieren) ist älter als die Version des nativen Treibers. Das heißt, findOneAndUpdate() war ursprünglich keine native Treiberfunktion, sondern eine von Mongoose bereitgestellte, sodass Mongoose hinter den Kulissen vom Treiber findOneAndUpdate findAndModify zu erstellen. Da der Treiber jetzt aktualisiert wurde, enthält er eine eigene solche Funktion, sodass wir findAndModify nicht verwenden müssen. Das macht vielleicht keinen Sinn, und das ist in Ordnung – es ist keine wichtige Information im Maßstab der Dinge.

Schließlich hat MongoDB sein altes Server- und Engine-Überwachungssystem eingestellt. Wir verwenden die neue Methode mit useUnifiedTopology: true .

Was wir bisher haben, ist eine Möglichkeit, sich mit der Datenbank zu verbinden. Aber hier ist die Sache – es ist nicht skalierbar oder effizient. Wenn wir Unit-Tests für diese API schreiben, verwenden die Unit-Tests ihre eigenen Testdaten (oder Fixtures) in ihren eigenen Testdatenbanken. Wir wollen also eine Möglichkeit, Verbindungen für verschiedene Zwecke zu erstellen – einige für Testumgebungen (die wir nach Belieben hoch- und herunterfahren können), andere für Entwicklungsumgebungen und andere für Produktionsumgebungen. Dazu bauen wir eine Fabrik. (Erinnerst du dich daran von früher?)

Verbindung zu Mongo – Erstellen einer Implementierung einer JS-Factory

Tatsächlich sind Java-Objekte überhaupt nicht analog zu JavaScript-Objekten, und daher trifft das, was wir oben aus dem Factory-Design-Pattern wissen, nicht zu. Ich habe das nur als Beispiel angegeben, um das traditionelle Muster zu zeigen. Um ein Objekt in Java, C# oder C++ usw. zu erhalten, müssen wir eine Klasse instanziieren. Dies geschieht mit dem Schlüsselwort new , das den Compiler anweist, Speicher für das Objekt auf dem Heap zuzuweisen. In C++ gibt uns dies einen Zeiger auf das Objekt, das wir selbst bereinigen müssen, damit wir keine hängenden Zeiger oder Speicherlecks haben (C++ hat keinen Garbage Collector, im Gegensatz zu Node/V8, das auf C++ aufbaut). oben muss nicht getan werden – wir müssen keine Klasse instanziieren, um ein Objekt zu erhalten – ein Objekt ist nur {} . Einige Leute werden sagen, dass alles in JavaScript ein Objekt ist, obwohl das technisch gesehen nicht stimmt, weil primitive Typen keine Objekte sind.

Aus den oben genannten Gründen wird unsere JS-Factory einfacher sein und an der losen Definition einer Factory festhalten, die eine Funktion ist, die ein Objekt (ein JS-Objekt) zurückgibt. Da eine Funktion ein Objekt ist (für eine function die von einem object über prototypische Vererbung erbt), wird unser Beispiel unten dieses Kriterium erfüllen. Um die Factory zu implementieren, erstelle ich innerhalb des server einen neuen Ordner mit dem Namen db . Innerhalb von db erstelle ich eine neue Datei namens mongoose.js . Diese Datei stellt Verbindungen zur Datenbank her. Innerhalb von mongoose.js erstelle ich eine Funktion namens connectionFactory und exportiere sie standardmäßig:

 // Directory - server/db/mongoose.js const mongoose = require('mongoose'); const MONGODB_URL = 'Your MongoDB URL'; const connectionFactory = () => { return mongoose.connect(MONGODB_URL, { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false }); }; module.exports = connectionFactory;

Unter Verwendung der von ES6 bereitgestellten Abkürzung für Pfeilfunktionen, die eine Anweisung in derselben Zeile wie die Methodensignatur zurückgeben, mache ich diese Datei einfacher, indem ich die connectionFactory -Definition entferne und standardmäßig nur die Factory exportiere:

 // server/db/mongoose.js const mongoose = require('mongoose'); const MONGODB_URL = 'Your MongoDB URL'; module.exports = () => mongoose.connect(MONGODB_URL, { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: true });

Jetzt muss man nur noch die Datei anfordern und die Methode aufrufen, die exportiert wird, wie folgt:

 const connectionFactory = require('./db/mongoose'); connectionFactory(); // OR require('./db/mongoose')();

Sie könnten die Kontrolle umkehren, indem Sie Ihre MongoDB-URL als Parameter für die Factory-Funktion bereitstellen, aber wir werden die URL als Umgebungsvariable basierend auf der Umgebung dynamisch ändern.

Die Vorteile unserer Verbindung als Funktion bestehen darin, dass wir diese Funktion später im Code aufrufen können, um eine Verbindung zur Datenbank von Dateien herzustellen, die für die Produktion und für lokale und Remote-Integrationstests sowohl auf dem Gerät als auch mit einer Remote-CI/CD-Pipeline bestimmt sind /server erstellen.

Aufbau unserer Endpunkte

Wir fangen jetzt an, unseren Endpunkten eine sehr einfache CRUD-bezogene Logik hinzuzufügen. Wie bereits erwähnt, ist ein kurzer Haftungsausschluss angebracht. Die Methoden, mit denen wir hier unsere Geschäftslogik implementieren, sollten Sie nicht für andere als einfache Projekte spiegeln. Das Herstellen einer Verbindung zu Datenbanken und das Ausführen von Logik direkt in Endpunkten ist (und sollte) verpönt sein, da Sie die Möglichkeit verlieren, Dienste oder DBMSs auszutauschen, ohne eine anwendungsweite Umgestaltung durchführen zu müssen. Da dies jedoch ein Artikel für Anfänger ist, wende ich diese schlechten Praktiken hier an. Ein zukünftiger Artikel dieser Reihe wird diskutieren, wie wir sowohl die Komplexität als auch die Qualität unserer Architektur steigern können.

Lassen Sie uns vorerst zu unserer server.js -Datei zurückkehren und sicherstellen, dass wir beide denselben Ausgangspunkt haben. Beachten Sie, dass ich die require -Anweisung für unsere Datenbankverbindungsfactory hinzugefügt und das Modell importiert habe, das wir aus ./models/book.js exportiert haben.

 const express = require('express'); // Database connection and model. require('./db/mongoose.js'); const Book = require('./models/book.js'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // HTTP POST /books app.post('/books', (req, res) => { // ... console.log('A POST Request was made!'); }); // HTTP GET /books/:id app.get('/books/:id', (req, res) => { // ... console.log(`A GET Request was made! Getting book ${req.params.id}`); }); // HTTP PATCH /books/:id app.patch('/books/:id', (req, res) => { // ... console.log(`A PATCH Request was made! Updating book ${req.params.id}`); }); // HTTP DELETE /books/:id app.delete('/books/:id', (req, res) => { // ... console.log(`A DELETE Request was made! Deleting book ${req.params.id}`); }); // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

Ich werde mit app.post() beginnen. Wir haben Zugriff auf das Book , weil wir es aus der Datei exportiert haben, in der wir es erstellt haben. Wie in den Mongoose-Dokumenten angegeben, ist Book konstruierbar. Um ein neues Buch zu erstellen, rufen wir den Konstruktor auf und übergeben die Buchdaten wie folgt:

 const book = new Book(bookData);

In unserem Fall haben wir bookData als das in der Anfrage gesendete Objekt, das auf req.body.book verfügbar sein wird. Denken Sie daran, dass die express.json() Middleware alle JSON-Daten, die wir senden, an req.body . Wir sollen JSON im folgenden Format senden:

 { "book": { "title": "The Art of Computer Programming", "isbn": "ISBN-13: 978-0-201-89683-1", "author": { "firstName": "Donald", "lastName": "Knuth" }, "publishingDate": "July 17, 1997", "finishedReading": true } }

Das bedeutet also, dass der übergebene JSON-Code geparst wird und das gesamte JSON-Objekt (das erste Paar geschweifter Klammern) von der express.json() Middleware auf req.body platziert wird. Die einzige Eigenschaft unseres JSON-Objekts ist book , und daher ist das book Objekt auf req.body.book verfügbar.

An dieser Stelle können wir die Modellkonstruktorfunktion aufrufen und unsere Daten übergeben:

 app.post('/books', async (req, res) => { // <- Notice 'async' const book = new Book(req.body.book); await book.save(); // <- Notice 'await' });

Beachte hier ein paar Dinge. Das Aufrufen der save -Methode auf der Instanz, die wir vom Aufrufen der Konstruktorfunktion zurückbekommen, wird das req.body.book Objekt in der Datenbank beibehalten, wenn und nur wenn es dem Schema entspricht, das wir im Mongoose-Modell definiert haben. Das Speichern von Daten in einer Datenbank ist eine asynchrone Operation, und diese save() -Methode gibt ein Versprechen zurück – auf dessen Einlösung wir sehr warten. Anstatt einen .then() -Aufruf zu verketten, verwende ich die ES6 Async/Await-Syntax, was bedeutet, dass ich die Callback-Funktion an app.post async machen muss.

book.save() lehnt mit einem ValidationError ab, wenn das vom Client gesendete Objekt nicht dem von uns definierten Schema entspricht. Unser aktuelles Setup sorgt für einen sehr unstabilen und schlecht geschriebenen Code, da wir nicht möchten, dass unsere Anwendung im Falle eines Fehlers bei der Validierung abstürzt. Um das zu beheben, werde ich die gefährliche Operation in eine try/catch -Klausel einschließen. Im Fehlerfall gebe ich eine HTTP 400 Bad Request oder eine HTTP 422 Unprocessable Entity zurück. Es gibt einige Diskussionen darüber, was verwendet werden soll, also bleibe ich bei 400 für diesen Artikel, da es allgemeiner ist.

 app.post('/books', async (req, res) => { try { const book = new Book(req.body.book); await book.save(); return res.status(201).send({ book }); } catch (e) { return res.status(400).send({ error: 'ValidationError' }); } });

Beachten Sie, dass ich die ES6-Objektkurzschrift verwende, um das book im Erfolgsfall mit res.send({ book }) direkt an den Client zurückzugeben – das wäre äquivalent zu res.send({ book: book }) . Ich gebe den Ausdruck auch zurück, nur um sicherzustellen, dass meine Funktion beendet wird. Im catch -Block setze ich den Status explizit auf 400 und gebe den String „ValidationError“ für die error des Objekts zurück, das zurückgesendet wird. A 201 ist der Erfolgspfad-Statuscode, der „ERSTELLT“ bedeutet.

Tatsächlich ist dies auch nicht die beste Lösung, da wir nicht wirklich sicher sein können, dass der Grund für das Scheitern eine Bad Request auf Seiten des Clients war. Möglicherweise haben wir die Verbindung zur Datenbank verloren (vermutlich eine abgebrochene Socket-Verbindung, also eine vorübergehende Ausnahme). In diesem Fall sollten wir wahrscheinlich einen 500 Internal Server-Fehler zurückgeben. Eine Möglichkeit, dies zu überprüfen, wäre, das Fehlerobjekt e zu lesen und selektiv eine Antwort zurückzugeben. Lassen Sie uns das jetzt tun, aber wie ich bereits mehrfach gesagt habe, wird ein Folgeartikel die richtige Architektur in Bezug auf Router, Controller, Dienste, Repositories, benutzerdefinierte Fehlerklassen, benutzerdefinierte Fehler-Middleware, benutzerdefinierte Fehlerantworten, Datenbankmodell-/Domänenentitätsdaten diskutieren Mapping und Command Query Separation (CQS).

 app.post('/books', async (req, res) => { try { const book = new Book(req.body.book); await book.save(); return res.send({ book }); } catch (e) { if (e instanceof mongoose.Error.ValidationError) { return res.status(400).send({ error: 'ValidationError' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

Fahren Sie fort und öffnen Sie Postman (vorausgesetzt, Sie haben es, laden Sie es andernfalls herunter und installieren Sie es) und erstellen Sie eine neue Anfrage. Wir stellen eine POST-Anforderung an localhost:3000/books . Unter der Registerkarte „Body“ im Abschnitt „Postman Request“ wähle ich das Optionsfeld „raw“ und im Drop-down-Menü ganz rechts „JSON“ aus. Dadurch wird der Anfrage automatisch der Header Content-Type: application/json hinzugefügt. Ich kopiere dann das JSON-Objekt „Book“ von früher und füge es in den Textbereich „Body“ ein. Das haben wir:

Die Postman-GUI wird mit Antwortdaten von der POST-Anforderung gefüllt.
JSON-Payload-Antwort auf unsere POST-Anfrage. (Große Vorschau)

Danach drücke ich auf die Schaltfläche „Senden“, und Sie sollten eine 201 Erstellt-Antwort im Abschnitt „Antwort“ von Postman (untere Reihe) sehen. Wir sehen dies, weil wir Express ausdrücklich gebeten haben, mit einem 201 und dem Book-Objekt zu antworten – hätten wir gerade res.send() ohne Statuscode ausgeführt, hätte express automatisch mit einem 200 OK geantwortet. Wie Sie sehen können, ist das Book-Objekt jetzt in der Datenbank gespeichert und wurde als Antwort auf die POST-Anforderung an den Client zurückgegeben.

Die Postman-GUI wird mit Daten für die POST-Anforderung gefüllt.
Daten zum Ausfüllen von Postman-Feldern für unsere POST-Anfrage. (Große Vorschau)

Wenn Sie die Buchsammlung der Datenbank über MongoDB Atlas anzeigen, sehen Sie, dass das Buch tatsächlich gespeichert wurde.

Sie können auch feststellen, dass MongoDB die Felder __v und _id eingefügt hat. Ersteres stellt die Version des Dokuments dar, in diesem Fall 0, und letzteres ist die ObjectID des Dokuments – die automatisch von MongoDB generiert wird und garantiert eine geringe Kollisionswahrscheinlichkeit aufweist.

Eine Zusammenfassung dessen, was wir bisher behandelt haben

Wir haben bisher viel in dem Artikel behandelt. Gönnen wir uns eine kurze Verschnaufpause, indem wir eine kurze Zusammenfassung durchgehen, bevor wir zum Abschluss der Express-API zurückkehren.

Wir haben etwas über ES6 Object Destructuring, die ES6 Object Shorthand Syntax sowie den ES6 Rest/Spread-Operator gelernt. Alle drei lassen uns Folgendes tun (und mehr, wie oben besprochen):

 // Destructuring Object Properties: const { a: newNameA = 'Default', b } = { a: 'someData', b: 'info' }; console.log(`newNameA: ${newNameA}, b: ${b}`); // newNameA: someData, b: info // Destructuring Array Elements const [elemOne, elemTwo] = [() => console.log('hi'), 'data']; console.log(`elemOne(): ${elemOne()}, elemTwo: ${elemTwo}`); // elemOne(): hi, elemTwo: data // Object Shorthand const makeObj = (name) => ({ name }); console.log(`makeObj('Tim'): ${JSON.stringify(makeObj('Tim'))}`); // makeObj('Tim'): { "name": "Tim" } // Rest, Spread const [c, d, ...rest] = [0, 1, 2, 3, 4]; console.log(`c: ${c}, d: ${d}, rest: ${rest}`) // c: 0, d: 1, rest: 2, 3, 4

Wir haben auch Express, Expess-Middleware, Server, Ports, IP-Adressierung usw. behandelt. Interessant wurde es, als wir erfuhren, dass es Methoden gibt, die für das Rückgabeergebnis von require('express')(); mit den Namen der HTTP-Verben wie app.get und app.post .

Wenn dieser Teil require('express')() für Sie keinen Sinn ergab, war dies der Punkt, auf den ich hinauswollte:

 const express = require('express'); const app = express(); app.someHTTPVerb

Es sollte genauso sinnvoll sein, wie wir zuvor die Verbindungsfabrik für Mongoose abgefeuert haben.

Jeder Routenhandler, der die Endpunktfunktion (oder Callback-Funktion) ist, wird in einem req Objekt und einem res -Objekt von Express hinter den Kulissen übergeben. (Technisch gesehen sind sie auch next , wie wir gleich sehen werden). req enthält Daten, die für die eingehende Anfrage vom Client spezifisch sind, wie z. B. Header oder gesendete JSON-Dateien. res ermöglicht es uns, Antworten an den Kunden zurückzusenden. Die next Funktion wird ebenfalls an Handler übergeben.

Bei Mongoose haben wir gesehen, wie wir mit zwei Methoden eine Verbindung zur Datenbank herstellen können – eine primitive Methode und eine fortgeschrittenere/praktischere Methode, die Anleihen beim Factory Pattern macht. Wir werden dies letztendlich verwenden, wenn wir mit Jest über Einheiten- und Integrationstests (und Mutationstests) sprechen, da wir damit eine Testinstanz der DB erstellen können, die mit Seed-Daten gefüllt ist, für die wir Assertionen ausführen können.

Danach haben wir ein Mongoose-Schemaobjekt erstellt und es zum Erstellen eines Modells verwendet und dann gelernt, wie wir den Konstruktor dieses Modells aufrufen können, um eine neue Instanz davon zu erstellen. Auf der Instanz ist (unter anderem) eine save verfügbar, die asynchroner Natur ist und prüft, ob die von uns übergebene Objektstruktur mit dem Schema übereinstimmt, das Versprechen auflöst, wenn dies der Fall ist, und das Versprechen mit einem ValidationError zurückweist, wenn dies der Fall ist Es tut nicht. Im Falle einer Auflösung wird das neue Dokument in der Datenbank gespeichert und wir antworten mit HTTP 200 OK/201 CREATED, andernfalls fangen wir den ausgelösten Fehler in unserem Endpunkt ab und senden eine HTTP 400 Bad Request an den Client zurück.

Während wir mit dem Aufbau unserer Endpunkte fortfahren, erfahren Sie mehr über einige der Methoden, die für das Modell und die Modellinstanz verfügbar sind.

Fertigstellung unserer Endpunkte

Nachdem wir den POST-Endpunkt fertiggestellt haben, behandeln wir GET. Wie ich bereits erwähnt habe, lässt die :id -Syntax innerhalb der Route Express wissen, dass id ein Routenparameter ist, auf den über req.params . Sie haben bereits gesehen, dass, wenn Sie eine ID für den Parameter „Wildcard“ in der Route abgleichen, diese in den frühen Beispielen auf dem Bildschirm ausgegeben wurde. Wenn Sie beispielsweise eine GET-Anfrage an „/books/test-id-123“ gestellt haben, dann wäre req.params.id die Zeichenfolge test-id-123 , da der Parametername id war, indem die Route als HTTP GET /books/:id .

Alles, was wir tun müssen, ist, diese ID aus dem req Objekt abzurufen und zu prüfen, ob irgendein Dokument in unserer Datenbank dieselbe ID hat – etwas, das durch Mongoose (und den nativen Treiber) sehr einfach gemacht wird.

 app.get('/books/:id', async (req, res) => { const book = await Book.findById(req.params.id); console.log(book); res.send({ book }); });

Sie können sehen, dass in unserem Modell eine Funktion verfügbar ist, die wir aufrufen können, um ein Dokument anhand seiner ID zu finden. Hinter den Kulissen wandelt Mongoose jede ID, die wir an findById , in den Typ des _id im Dokument um, oder in diesem Fall in eine ObjectId . Wenn eine übereinstimmende ID gefunden wird (und nur eine wird jemals gefunden, ObjectId eine extrem niedrige Kollisionswahrscheinlichkeit hat), wird dieses Dokument in unsere Konstantenvariable book gestellt. Wenn nicht, ist book null – eine Tatsache, die wir in naher Zukunft verwenden werden.

Lassen Sie uns zunächst den Server neu starten (Sie müssen den Server neu starten, es sei denn, Sie verwenden nodemon ) und sicherstellen, dass wir immer noch das eine Buchdokument von vorher in der Books haben. Fahren Sie fort und kopieren Sie die ID dieses Dokuments, den hervorgehobenen Teil des Bildes unten:

Die Objekt-ID des Buchdokuments
Ein Beispiel für eine ObjectID, die für die kommende GET-Anforderung verwendet werden soll. (Große Vorschau)

Und verwenden Sie es, um eine GET-Anfrage an /books/:id mit Postman wie folgt zu stellen (beachten Sie, dass die Körperdaten nur von meiner früheren POST-Anfrage übrig geblieben sind. Sie werden nicht wirklich verwendet, obwohl sie im Bild unten dargestellt sind). :

Die Postman-GUI wird mit Daten für die GET-Anforderung gefüllt.
API-URL und Postman-Daten für GET-Anfrage. (Große Vorschau)

Danach sollten Sie das Buchdokument mit der angegebenen ID wieder im Antwortbereich des Postboten erhalten. Beachten Sie, dass wir früher bei der POST-Route, die zum „POSTEN“ oder „Pushen“ neuer Ressourcen auf den Server entwickelt wurde, mit einem 201 Created geantwortet haben – weil eine neue Ressource (oder ein neues Dokument) erstellt wurde. Im Fall von GET wurde nichts Neues erstellt – wir haben nur eine Ressource mit einer bestimmten ID angefordert, daher haben wir einen 200 OK-Statuscode anstelle von 201 Erstellt zurückerhalten.

Wie im Bereich der Softwareentwicklung üblich, müssen Grenzfälle berücksichtigt werden – Benutzereingaben sind von Natur aus unsicher und fehlerhaft, und es ist unsere Aufgabe als Entwickler, flexibel auf die Arten von Eingaben zu reagieren, die uns gegeben werden können entsprechend. Was tun wir, wenn der Benutzer (oder der API-Aufrufer) uns eine ID übergibt, die nicht in eine MongoDB-Objekt-ID umgewandelt werden kann, oder eine ID, die umgewandelt werden kann, aber nicht existiert?

Für den ersten Fall wird Mongoose einen CastError werfen – was verständlich ist, denn wenn wir eine ID wie math-is-fun angeben, dann ist das offensichtlich nichts, was in eine ObjectID umgewandelt werden kann, und das Casting in eine ObjectID ist genau das, was Mongoose tut unter der Haube.

Im letzteren Fall könnten wir das Problem leicht über einen Null-Check oder eine Guard-Klausel beheben. In jedem Fall werde ich eine HTTP 404 Not Found-Antwort zurücksenden. Ich zeige Ihnen ein paar Möglichkeiten, wie wir dies tun können, eine schlechte und dann eine bessere.

Erstens könnten wir Folgendes tun:

 app.get('/books/:id', async (req, res) => { try { const book = await Book.findById(req.params.id); if (!book) throw new Error(); return res.send({ book }); } catch (e) { return res.status(404).send({ error: 'Not Found' }); } });

Das funktioniert und wir können es gut verwenden. Ich erwarte, dass die Anweisung await Book.findById() einen Mongoose CastError , wenn die ID-Zeichenfolge nicht in eine ObjectID umgewandelt werden kann, wodurch der catch -Block ausgeführt wird. Wenn es gecastet werden kann, aber die entsprechende ObjectID nicht existiert, dann ist book null und die Null-Prüfung gibt einen Fehler aus und löst erneut den catch -Block aus. Innerhalb von catch geben wir einfach 404 zurück. Hier gibt es zwei Probleme. Erstens, selbst wenn das Buch gefunden wird, aber ein anderer unbekannter Fehler auftritt, senden wir einen 404 zurück, obwohl wir dem Client wahrscheinlich einen generischen Catch-All 500 geben sollten. Zweitens unterscheiden wir nicht wirklich, ob die gesendete ID gültig ist, aber nicht existiert, oder ob es nur ein schlechter Ausweis ist.

Also, hier ist ein anderer Weg:

 const mongoose = require('mongoose'); app.get('/books/:id', async (req, res) => { try { const book = await Book.findById(req.params.id); if (!book) return res.status(404).send({ error: 'Not Found' }); return res.send({ book }); } catch (e) { if (e instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

Das Schöne daran ist, dass wir alle drei Fälle eines 400, eines 404 und eines generischen 500 behandeln können. Beachten Sie, dass ich nach der Nullprüfung auf book das Schlüsselwort return für meine Antwort verwende. Dies ist sehr wichtig, da wir sicherstellen möchten, dass wir den Routenhandler dort verlassen.

Einige andere Optionen könnten sein, dass wir prüfen, ob die id auf req.params explizit in eine ObjectID umgewandelt werden kann, anstatt Mongoose zu erlauben, implizit mit mongoose.Types.ObjectId.isValid('id); , aber es gibt einen Grenzfall mit 12-Byte-Strings, der dazu führt, dass dies manchmal unerwartet funktioniert.

Wir könnten diese Wiederholung beispielsweise mit Boom , einer HTTP-Antwortbibliothek, weniger schmerzhaft machen, oder wir könnten Middleware zur Fehlerbehandlung einsetzen. Wir könnten Mongoose Errors auch mit Mongoose Hooks/Middleware, wie hier beschrieben, in etwas besser Lesbares umwandeln. Eine zusätzliche Option wäre, benutzerdefinierte Fehlerobjekte zu definieren und globale Express-Fehlerbehandlungs-Middleware zu verwenden, aber ich hebe mir das für einen kommenden Artikel auf, in dem wir bessere Architekturmethoden diskutieren.

Im Endpunkt für PATCH /books/:id erwarten wir, dass ein Update-Objekt übergeben wird, das Updates für das betreffende Buch enthält. Für diesen Artikel lassen wir zu, dass alle Felder aktualisiert werden, aber in Zukunft werde ich zeigen, wie wir Aktualisierungen bestimmter Felder verbieten können. Außerdem werden Sie sehen, dass die Fehlerbehandlungslogik in unserem PATCH-Endpunkt dieselbe ist wie in unserem GET-Endpunkt. Das ist ein Hinweis darauf, dass wir gegen die DRY-Prinzipien verstoßen, aber darauf kommen wir später noch einmal zurück.

Ich gehe davon aus, dass alle Aktualisierungen im updates von req.body verfügbar sind (was bedeutet, dass der Client JSON mit einem updates sendet) und die Book.findByAndUpdate Funktion mit einem speziellen Flag verwenden, um die Aktualisierung durchzuführen.

 app.patch('/books/:id', async (req, res) => { const { id } = req.params; const { updates } = req.body; try { const updatedBook = await Book.findByIdAndUpdate(id, updates, { runValidators: true, new: true }); if (!updatedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: updatedBook }); } catch (e) { if (e instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

Beachte hier ein paar Dinge. Wir destrukturieren zuerst id von req.params und updates von req.body .

Für das Book Modell ist eine Funktion mit dem Namen findByIdAndUpdate , die die ID des betreffenden Dokuments, die auszuführenden Aktualisierungen und ein optionales Optionsobjekt entgegennimmt. Normalerweise führt Mongoose die Validierung für Aktualisierungsoperationen nicht erneut durch, also übergeben wir das Flag runValidators: true , da das options -Objekt es dazu zwingt. Darüber hinaus gibt Model.findByIdAndUpdate ab Mongoose 4 nicht mehr das geänderte Dokument, sondern stattdessen das Originaldokument zurück. Das Flag new: true (das standardmäßig false ist) setzt dieses Verhalten außer Kraft.

Schließlich können wir unseren DELETE-Endpunkt aufbauen, der allen anderen ziemlich ähnlich ist:

 app.delete('/books/:id', async (req, res) => { try { const deletedBook = await Book.findByIdAndDelete(req.params.id); if (!deletedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: deletedBook }); } catch (e) { if (e instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

Damit ist unsere primitive API vollständig und Sie können sie testen, indem Sie HTTP-Anforderungen an alle Endpunkte senden.

Ein kurzer Haftungsausschluss über Architektur und wie wir sie korrigieren

Aus architektonischer Sicht ist der Code, den wir hier haben, ziemlich schlecht, er ist chaotisch, er ist nicht TROCKEN, er ist nicht SOLID, man könnte ihn sogar als abscheulich bezeichnen. Diese sogenannten „Route Handler“ tun viel mehr als nur „Routen zu übergeben“ – sie sind direkt mit unserer Datenbank verbunden. Das heißt, es gibt absolut keine Abstraktion.

Seien wir ehrlich, die meisten Anwendungen werden niemals so klein sein, oder Sie könnten wahrscheinlich mit serverlosen Architekturen mit der Firebase-Datenbank davonkommen. Vielleicht möchten Benutzer, wie wir später sehen werden, die Möglichkeit haben, Avatare, Zitate und Ausschnitte aus ihren Büchern usw. hochzuladen. Vielleicht möchten wir eine Live-Chat-Funktion zwischen Benutzern mit WebSockets hinzufügen, und gehen wir sogar so weit, wir zu sagen wird unsere Anwendung öffnen, damit Benutzer gegen eine geringe Gebühr Bücher miteinander ausleihen können – an diesem Punkt müssen wir die Zahlungsintegration mit der Stripe-API und die Versandlogistik mit der Shippo-API in Betracht ziehen.

Angenommen, wir fahren mit unserer aktuellen Architektur fort und fügen all diese Funktionen hinzu. Diese Route-Hander, auch als Controller-Aktionen bekannt, werden am Ende sehr, sehr groß und haben eine hohe zyklomatische Komplexität . Ein solcher Codierungsstil mag uns in den frühen Tagen gut gefallen, aber was ist, wenn wir entscheiden, dass unsere Daten referenziell sind und PostgreSQL daher eine bessere Datenbankwahl ist als MongoDB? Wir müssen jetzt unsere gesamte Anwendung umgestalten, Mongoose entfernen, unsere Controller ändern usw. All dies könnte zu potenziellen Fehlern in der restlichen Geschäftslogik führen. Ein weiteres solches Beispiel wäre die Entscheidung, dass AWS S3 zu teuer ist und wir auf GCP migrieren möchten. Auch dies erfordert ein anwendungsweites Refactoring.

Obwohl es viele Meinungen zur Architektur gibt, von Domain-Driven Design, Command Query Responsibility Segregation und Event Sourcing bis hin zu Test-Driven Development, SOILD, Layered Architecture, Onion Architecture und mehr, konzentrieren wir uns auf die Implementierung einer einfachen Layered Architecture in zukünftige Artikel, bestehend aus Controllern, Diensten und Repositories, die Entwurfsmuster wie Komposition, Adapter/Wrapper und Inversion of Control via Dependency Injection verwenden. Während dies bis zu einem gewissen Grad mit JavaScript durchgeführt werden könnte, werden wir uns auch mit TypeScript-Optionen befassen, um diese Architektur zu erreichen, die es uns ermöglicht, funktionale Programmierparadigmen wie Both Monads zusätzlich zu OOP-Konzepten wie Generics einzusetzen.

Im Moment gibt es zwei kleine Änderungen, die wir vornehmen können. Da unsere Fehlerbehandlungslogik im catch -Block aller Endpunkte ziemlich ähnlich ist, können wir sie in eine benutzerdefinierte Express-Fehlerbehandlungs-Middleware-Funktion ganz am Ende des Stapels extrahieren.

Bereinigen Sie unsere Architektur

Derzeit wiederholen wir eine sehr große Menge an Fehlerbehandlungslogik auf allen unseren Endpunkten. Stattdessen können wir eine Express-Middleware-Funktion zur Fehlerbehandlung erstellen, bei der es sich um eine Express-Middleware-Funktion handelt, die mit einem Fehler, den req- und res-Objekten und der nächsten Funktion aufgerufen wird.

Lassen Sie uns zunächst diese Middleware-Funktion erstellen. Alles, was ich tun werde, ist die gleiche Fehlerbehandlungslogik zu wiederholen, an die wir gewöhnt sind:

 app.use((err, req, res, next) => { if (err instanceof mongoose.Error.ValidationError) { return res.status(400).send({ error: 'Validation Error' }); } else if (err instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { console.log(err); // Unexpected, so worth logging. return res.status(500).send({ error: 'Internal error' }); } });

Dies scheint bei Mongoose-Fehlern nicht zu funktionieren, aber im Allgemeinen können Sie, anstatt if/else if/else zu verwenden, um Fehlerinstanzen zu bestimmen, den Konstruktor des Fehlers umschalten. Ich lasse jedoch, was wir haben.

Wenn Sie in einem synchronen Endpunkt-/Routen-Handler einen Fehler ausgeben, fängt Express ihn ab und verarbeitet ihn, ohne dass Ihrerseits zusätzliche Arbeit erforderlich ist. Leider ist das bei uns nicht der Fall. Wir haben es mit asynchronem Code zu tun. Um die Fehlerbehandlung mit asynchronen Routenhandlern an Express zu delegieren, fangen wir den Fehler oft selbst ab und übergeben ihn an next() .

Also lasse ich einfach next als drittes Argument im Endpunkt zu und entferne die Fehlerbehandlungslogik in den catch Blöcken zugunsten der Übergabe der Fehlerinstanz an next , als solche:

 app.post('/books', async (req, res, next) => { try { const book = new Book(req.body.book); await book.save(); return res.send({ book }); } catch (e) { next(e) } });

Wenn Sie dies für alle Routenhandler tun, sollten Sie am Ende den folgenden Code erhalten:

 const express = require('express'); const mongoose = require('mongoose'); // Database connection and model. require('./db/mongoose.js')(); const Book = require('./models/book.js'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // HTTP POST /books app.post('/books', async (req, res, next) => { try { const book = new Book(req.body.book); await book.save(); return res.status(201).send({ book }); } catch (e) { next(e) } }); // HTTP GET /books/:id app.get('/books/:id', async (req, res) => { try { const book = await Book.findById(req.params.id); if (!book) return res.status(404).send({ error: 'Not Found' }); return res.send({ book }); } catch (e) { next(e); } }); // HTTP PATCH /books/:id app.patch('/books/:id', async (req, res, next) => { const { id } = req.params; const { updates } = req.body; try { const updatedBook = await Book.findByIdAndUpdate(id, updates, { runValidators: true, new: true }); if (!updatedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: updatedBook }); } catch (e) { next(e); } }); // HTTP DELETE /books/:id app.delete('/books/:id', async (req, res, next) => { try { const deletedBook = await Book.findByIdAndDelete(req.params.id); if (!deletedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: deletedBook }); } catch (e) { next(e); } }); // Notice - bottom of stack. app.use((err, req, res, next) => { if (err instanceof mongoose.Error.ValidationError) { return res.status(400).send({ error: 'Validation Error' }); } else if (err instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { console.log(err); // Unexpected, so worth logging. return res.status(500).send({ error: 'Internal error' }); } }); // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

Im weiteren Verlauf wäre es sinnvoll, unsere Fehlerbehandlungs-Middleware in eine andere Datei aufzuteilen, aber das ist trivial, und wir werden es in zukünftigen Artikeln dieser Serie sehen. Zusätzlich könnten wir ein NPM-Modul namens express-async-errors , damit wir nicht next im catch-Block aufrufen müssen, aber ich versuche Ihnen noch einmal zu zeigen, wie die Dinge offiziell gemacht werden.

Ein Wort zu CORS und der Richtlinie zur gleichen Herkunft

Angenommen, Ihre Website wird von der Domain myWebsite.com , Ihr Server befindet sich jedoch unter myOtherDomain.com/api . CORS steht für Cross-Origin Resource Sharing und ist ein Mechanismus, mit dem domänenübergreifende Anfragen durchgeführt werden können. Da sich im obigen Fall der Server- und der Front-End-JS-Code in unterschiedlichen Domänen befinden, würden Sie eine Anfrage über zwei verschiedene Ursprünge stellen, was normalerweise aus Sicherheitsgründen vom Browser eingeschränkt und durch die Bereitstellung bestimmter HTTP-Header entschärft wird.

Die Same Origin Policy führt diese oben genannten Einschränkungen durch – ein Webbrowser lässt nur zu, dass Anforderungen über denselben Ursprung gestellt werden.

Wir werden später auf CORS und SOP eingehen, wenn wir mit React ein gebündeltes Webpack-Front-End für unsere Buch-API erstellen.

Fazit und wie es weiter geht

Wir haben in diesem Artikel viel diskutiert. Vielleicht war es nicht ganz praktikabel, aber es hat Ihnen hoffentlich mehr Komfort bei der Arbeit mit Express- und ES6-JavaScript-Funktionen gebracht. Wenn Sie neu in der Programmierung sind und Node der erste Weg ist, den Sie einschlagen, haben die Verweise auf Sprachen mit statischen Typen wie Java, C++ und C# hoffentlich dazu beigetragen, einige der Unterschiede zwischen JavaScript und seinen statischen Gegenstücken hervorzuheben.

Next time, we'll finish building out our Book API by making some fixes to our current setup with regards to the Book Routes, as well as adding in User Authentication so that users can own books. We'll do all of this with a similar architecture to what I described here and with MongoDB for data persistence. Finally, we'll permit users to upload avatar images to AWS S3 via Buffers.

In the article thereafter, we'll be rebuilding our application from the ground up in TypeScript, still with Express. We'll also move to PostgreSQL with Knex instead of MongoDB with Mongoose as to depict better architectural practices. Finally, we'll update our avatar image uploading process to use Node Streams (we'll discuss Writable, Readable, Duplex, and Transform Streams). Along the way, we'll cover a great amount of design and architectural patterns and functional paradigms, including:

  • Controllers/Controller Actions
  • Dienstleistungen
  • Repositories
  • Data Mapping
  • The Adapter Pattern
  • The Factory Pattern
  • The Delegation Pattern
  • OOP Principles and Composition vs Inheritance
  • Inversion of Control via Dependency Injection
  • SOLID Principles
  • Coding against interfaces
  • Data Transfer Objects
  • Domain Models and Domain Entities
  • Either Monads
  • Validierung
  • Decorators
  • Logging and Logging Levels
  • Unit Tests, Integration Tests (E2E), and Mutation Tests
  • The Structured Query Language
  • Relations
  • HTTP/Express Security Best Practices
  • Node Best Practices
  • OWASP Security Best Practices
  • Und mehr.

Using that new architecture, in the article after that, we'll write Unit, Integration, and Mutation tests, aiming for close to 100 percent testing coverage, and we'll finally discuss setting up a remote CI/CD pipeline with CircleCI, as well as Message Busses, Job/Task Scheduling, and load balancing/reverse proxying.

Hopefully, this article has been helpful, and if you have any queries or concerns, let me know in the comments below.