Backend - REST-Server¶
Ehe wir uns der IndexedDB-API zuwenden, erstellen wir zunächst eine "richtige" Datenbank für unsere Posts. Für diese Datenbank stellen wir die Implementierung einer Schnittstelle bereit, so dass wir die wesentlichen Datenbankanfragen darüber ausführen können. Diese wesentlichen Datenbankfragen werden mit CRUD abgekürzt, für Create, Read, Update und Delete. Das bedeutet, wir implementieren Funktionalitäten, mit denen wir einen neuen post
in die Datenbank einfügen (create), aus der Datenbank auslesen (read), in der Datenbank aktualisieren (update) und aus der Datenbank löschen (delete) können.
Die Schnittstelle, die wir implementieren, ist eine sogenannte REST-API. REST steht für Representational State Transfer und basiert auf einigen wenigen Prinzipien:
- Alles wird als eine Ressource betrachtet, z.B.
post
. - Jede Ressource ist durch URIs (Uniform Resource Identifiers) eindeutig identifizierbar, z.B.
http://localhost/posts
. - Es werden die Standard-HTTP-Methoden verwendet, also
GET
,POST
,PUT
,UPDATE
. - Ressourcen können in verschiedenen Formaten vorliegen, z.B. in HTML, XML, JSON, ...
- Die Kommunikation ist zustandslos. Jede einzelne HTTP-Anfrage wird komplett isoliert bearbeitet. Es gibt keinerlei Anfragehistorie.
Das bedeutet, wir erstellen ein Backend (einen REST-Server), an den HTTP-Anfragen mit der eindeutig identifizierbaren Ressource gestellt werden. Das Backend erstellt daraus die entsprechende SQL-Query. Das Resultat der Datenbankanfrage wird im JSON
- oder HTML
- oder XML
- oder in einem anderen Format bereitsgestellt.
Prinzipiell gibt es also ein Mapping von HTTP-Anfragen auf SQL-Anfragen:
CRUD | SQL | MongoDB | HTTP |
---|---|---|---|
create | INSERT | insertOne(), insertMany() | POST |
read | SELECT | findOne(), find() | GET |
update | UPDATE | updateOne(), updateMany() | PUT (oder PATCH) |
delete | DELETE | deleteOne(), deleteMany() | DELETE |
Zur Unterscheidung zwischen PUT
und PATCH
siehe z.B. hier oder hier.
Wir wollen uns ein Backend erstellen, über das wir unsere Daten verwalten. Dazu überlegen wir uns zunächst ein paar sogenannte Endpunkte (siehe Prinzipien von REST oben) und die Zugriffsmethoden, mit denen wir auf unsere Daten zugreifen wollen.
Methode | URL | Bedeutung |
---|---|---|
GET | /posts | hole alle Datensätze |
GET | /posts/11 | hole den Datensatz mit der id=11 |
POST | /posts | füge einen neuen Datensatz hinzu |
PUT | /posts/11 | ändere den Datensatz mit der id=11 |
DELETE | /posts/11 | lösche den Datensatz mit der id=11 |
Der Wert der id
ist natürlich nur ein Beispiel. Es soll für alle id
-Werte funktionieren, die in unserem Datensatz enthalten sind. Korrekterweise beschreiben wir die Endpunkte mit variabler id
besser durch /posts/:id
oder /posts/{id}
.
OpenAPI¶
Hier geht es zunächst um die Dokumentation der zu erstellenden REST-API. Wenn Sie an der Dokumentation nicht interssiert sind, können Sie auch direkt zur Implementierung springen. Eine ordentliche Dokumentation Ihrer REST-API ist jedoch immer gut und richtig. Es lässt sich daraus sogar bereits Code erzeugen.
Für eine Dokumentation der zu erstellenden REST-API ist OpenAPI geeignet. Unter https://app.swaggerhub.com/home steht ein Werkzeug zur Verfügung, um eine solche API-Dokumentation zu erstellen. Sie müssen sich dort registrieren und einloggen. Klicken Sie Create New
, um eine neue Dokumentation zu beginnen:
Es wird automatisch erstellt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
|
Zur Erläuterung:
- Unter dem Schlüssel
paths
(Zeile20
) ist ein Pfad (eine Route) definiert, nämlich/inventory
. Bei diesem Pfad handelt es sich um ein sogenanntes Path Item Object -
Der Pfad
/inventory
enthält zwei sogenannte Operation Objects, nämlichGET
(Zeile22
) undPOST
(Zeile63
). Ein Operation Object kann verschiedene Eigenschaften beinhalten:tags
: Schlüsselwörter, um die API-Dokumentation zu gruppieren (siehe unten im BildDokumentation der REST-API
die Gruppenadmins
unddevelopers
)summary
: dient der Erläuterung eines Endpunktes (siehe unten im BildDokumentation der REST-API
die Erläuterungensearches inventory
undadds an inventory item
)description
: beschreibt die Funktionalität des Endpunktes detaillierter. Erscheint in der Dokumentation bei den Details eines Endpunktes (siehe unten im BildGet /inventroy-Endpunkt im Detail
)responses
: beschreibt die Rückgabe des Endpunktes. Es handelt sich um ein Responses Object. Diese können nach HTTP-Statuscodes unterteilt werden. Neben derdescription
für den Statuscode kann dabei insbesondere der Typ derresponses
definiert werden. In dercontent
-Eigenschaft wird zunächst der Typ der akzeptierten Response definiert, z.B.application/json
oderimage/png
. Dann wird spezifiziert, welcher Datentyp zurückgeben wird. Die Zeilen57-60
beschrieben bspw., dass ein Array vonInventoryItems
zurückgegeben wird. Ein solchesInventoryItem
ist unter der Eigenschaftschemas
definiert. Mithilfe von$ref: '#/components/schemas/InventoryItem'
wird auf dieses Schema referenziert.- Unter dem Schlüssel
components
könnenschemas
,responses
,parameters
,examples
,requestBodies
,headers
usw. spezifiziert werden. Mithilfe von$ref
kann dann auf jede dieser Komponenten referenziert werden. In obigem Beispiel wurde das SchemaInventoryItem
und das SchemaManufacturer
definiert. Diese Schemen entsprechen den verwendeten Datenmodellen.
YAML¶
Die obige Beschreibung ist übrigens in YAML. Ursprünglich stand YAML für Yet Anaother Markup Language. Jetzt sagt die Spezifikation von YAML aber YAML Ain't Markup Language. Es hat Ähnlichkeiten zu JSON, kommt allerdings ohne Klammerung aus. Dafür spielt das Einrücken eine Rolle. OpenAPI unterstützt sowohl JSON als auch YAML.
Die '/posts'-Routen¶
Wir spezifizieren zunächst die /posts
-Routen, also GET /posts
und POST /posts
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
|
Die '/posts/{id}'-Routen¶
Nun fügen wir noch die /posts/{id}
-Routen hinzu, also GET /posts/{id}
, PUT /posts/{id}
und DELETE /posts/{id}
.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
|
Die vollständige YAML-Datei¶
Hier die vollständige YAML-Datei, die Sie auch in Postman importieren können.
openapi.yaml
openapi: 3.0.0
info:
title: Posts-API
description: REST-API für IKT-PWA HTWInsta
contact:
email: freiheit@htw-berlin.de
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
version: 1.0.0
servers:
- url: https://virtserver.swaggerhub.com/jfreiheit/Posts-API/1.0.0
description: SwaggerHub Server
tags:
- name: Posts
description: CRUD für Posts
- name: Images
description: CRUD für Bilder
paths:
/posts:
get:
tags:
- Posts
summary: lese alle Posts
description: download Array aller verfügbaren Posts
operationId: getAllPosts
responses:
"200":
description: alle verfügbaren Posts geladen
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Post'
x-content-type: application/json
x-swagger-router-controller: Posts
post:
tags:
- Posts
summary: füge einen neuen Post hinzu
description: neuen Post erzeugen und speichern
operationId: createNewPost
requestBody:
description: neuer Post
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
responses:
"201":
description: Post created
"409":
description: Post existiert bereits
x-swagger-router-controller: Posts
/posts/{id}:
get:
tags:
- Posts
summary: lese einen Post mit der passenden id
description: download entsprechenden Post
operationId: getOnePost
parameters:
- name: id
in: path
description: Post-ID
required: true
style: simple
explode: false
schema:
type: string
responses:
"200":
description: Post mit entsprechender id geladen
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Post'
x-content-type: application/json
"404":
description: Post bzw. id nicht gefunden
x-swagger-router-controller: Posts
put:
tags:
- Posts
summary: ändere einen Post mit der passenden id
description: aktualisiere entsprechenden Post
operationId: updateOnePost
parameters:
- name: id
in: path
description: Post-ID
required: true
style: simple
explode: false
schema:
type: string
requestBody:
description: zu aktualisierender Post
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
responses:
"200":
description: Post mit entsprechender id aktualisiert
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Post'
x-content-type: application/json
"404":
description: Post bzw. id nicht gefunden
x-swagger-router-controller: Posts
delete:
tags:
- Posts
summary: lösche einen Post mit der passenden id
description: lösche entsprechenden Post
operationId: deleteOnePost
parameters:
- name: id
in: path
description: Post-ID
required: true
style: simple
explode: false
schema:
type: string
responses:
"200":
description: Post mit entsprechender id gelöscht
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Post'
x-content-type: application/json
"404":
description: Post bzw. id nicht gefunden
x-swagger-router-controller: Posts
components:
schemas:
Post:
required:
- image_id
- location
- title
type: object
properties:
title:
type: string
example: H-Gebäude
location:
type: string
example: Campus Wilhelminenhof
image_id:
type: string
example: Campus Wilhelminenhof
example:
location: Campus Wilhelminenhof
title: H-Gebäude
image_id: Campus Wilhelminenhof
Ein Node.js-Projekt mit Express¶
Wir starten damit, uns ein node.js
-Projekt zu erstellen. Dazu erstellen wir uns zunächst einen Ordner backend
, wechseln in diesen Ordner und führen dann npm init
aus:
mkdir backend
cd backend
npm init
Sie werden ein paar Sachen gefragt. Im Prinzip können Sie immer Enter
drücken, außer beim entry point
. Dort können Sie gleich server.js
eingeben. Sie können das aber auch noch später in der package.json
ändern.
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help init` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (backend)
version: (1.0.0)
description: Backend REST-API
entry point: (index.js) server.js
test command:
git repository:
keywords: rest api backend mongodb
author: J. Freiheit
license: (ISC)
About to write to /Users/jornfreiheit/Sites/IKT22/05_Backend/00_skript/backend/package.json:
{
"name": "backend",
"version": "1.0.0",
"description": "Backend REST-API",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"rest",
"api",
"backend",
"mongodb"
],
"author": "J. Freiheit",
"license": "ISC"
}
Is this OK? (yes)
Die package.json
wurde erstellt. Nun benötigen wir noch das Modul Express. Express bietet uns eine unkomplizierte Middleware für die Weiterverwaltung von http
-Anfragen an die Datenbank und zurück.
npm install express --save
Die Option --save
muss eigentlich nicht mehr angegeben werden, aber unter Express steht es noch so. Sie erhalten eine Meldung in der Form:
% npm install express --save
added 57 packages, and audited 58 packages in 887ms
7 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
In der package.json
wurde die entsprechende Abhängigkeit eingetragen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
server.js erstellen und implementieren¶
Öffnen Sie nun das backend
-Projekt in Ihrer IDE und erstellen Sie sich dort eine Datei server.js
mit folgendem Inhalt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Das bedeutet, wir importieren express
(Zeile 1
), erzeugen uns davon ein Objekt und speichern dieses in der Variablen app
(Zeile 4
). Wir legen in einer Konstanten PORT
die Portnummer 3000
fest (Zeile 5
- die Portnummer können Sie wählen). Das backend
ist somit unter http://localhost:3000
verfügbar. Das eigentliche Starten des Webservers erfolgt in den Zeilen 10-16
durch Aufruf der listen()
-Funktion von express
. Die Syntax der listen()
-Funktion ist generell wie folgt:
app.listen([port[, host[, backlog]]][, callback])
Wir übergeben als ersten Parameter die PORT
-Nummer (3000
) und als zweiten Parameter eine (anonyme) Funktion als sogenannten callback. Callbacks sind hier näher erläutert. Die anonyme Funktion wird durch die listen()
-Funktion aufgerufen. Sollte ein Fehler aufgetreten sein (z.B. wenn der Port bereits belegt ist), wird der anonymen Funktion ein error
-Objekt übergeben. Ist das der Fall, wird der Fehler auf der Konsole ausgegeben. Wird der anonymen Funktion kein Objekt übergeben, wurde der Webserver korrekt gestartet und die entsprechende Meldung erscheint auf der Konsole.
Beachten Sie auch die verwendete Syntax ${PORT}
im sogenannte template literal. Beachten Sie, dass template literals nicht in einfachen ('
) oder doppelten ("
) Anführungsstrichen stehen, sondern in `
(backticks).
Router¶
Noch lässt sich unser Programm aber nicht ausführen. Wir benötigen im Projektordner noch einen Ordner routes
und darin eine Datei post.routes.js
. Diese wird nämlich in der server.js
bereits in Zeile 2
eingebunden und in Zeile 8
verwendet.
1 2 3 4 5 6 7 8 9 10 |
|
Beim Router
handelt es sich um eine Middleware (siehe hier), die die Routen verwaltet und request
-Objekte an die entsprechende Routen weiterleitet und response
-Objekte empfängt. In unserer post.routes.js
haben wir zunächst eine GET
-Anfrage implementiert (Zeile 5
). Das request
-Objekt heißt hier req
. Das verwenden wir aber gar nicht. Das respones
-Objekt heißt hier res
und wird durch die Anfrage erzeugt. Wir senden in der response
ein JavaScript-Objekt zurück, das einen Schlüssel message
enthält.
In der server.js
haben wir mit app.use(express.json())
(Zeile 7
) angegeben, dass alle JavaScript-Objekte in der response
nach JSON umgewandelt werden sollen. Wenn nun die URL localhost:3000
aufgerufen wird, dann wird ein request
ausgelöst, den wir hier mit Hello FIW!
als response
beantworten (Zeilen 5-8
).
Wichtig ist, dass wir router
mit module.exports
exportieren, damit es von anderen Modulen importiert und genutzt werden kann. Siehe dazu z.B. hier. Meine Empfehlung ist, (noch) nicht das neue ESM6-Format zu nutzen!
Noch "läuft" unser Backend aber noch nicht. Wir müssen es erst starten.
Starten des Projektes und Installation von nodemon¶
Das Projekt lässt sich nun starten. Wir geben dazu im Terminal im backend
-Ordner
node server.js
ein. Im Terminal erscheint
server running on http://localhost:3000
und wenn Sie im Browser die URL http://localhost:3000/
eingeben, wird dort
angezeigt. Sie können auch Postman öffnen und http://localhost:3000
eintragen (GET
-Methode):
Wann immer wir jetzt jedoch etwas an der Implementierung ändern, müssen wir im Terminal zunächst den Webserver mit
Strg-C // bzw. Control-C
stoppen, um ihn dann wieder mit node server.js
zu starten. Um das zu umgehen, gibt es das Paket nodemon. Da es nur sinnvoll während der Entwicklung eingesetzt werden kann (und sollte), installieren wir es als eine development dependency:
npm install --save-dev nodemon
Die package.json
sieht daraufhin so aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Zur Verwendung von nodemon
fügen wir in die package.json
unter "scripts"
noch die Eigenschaft watch
(frei gewählt) und den dazugehörigen Wert nodemon server.js
ein:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Nun lässt sich die Anwendung mithilfe von nodemon
per
npm run watch
starten und muss auch nicht mehr gestoppt und neu gestartet werden, wenn Änderungen an der Implementierungen durchgeführt wurden. Die Ausgabe im Terminal nach Eingabe von npm run watch
ist ungefähr so:
> backend@1.0.0 watch
> nodemon ./server.js
[nodemon] 2.0.22
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./server.js`
server running on http://localhost:3000
Hier nur zum Verständnis. Angenommen, wir ändern bspw. in der server.js
die Zeile 8
zu
app.use('/api', routes);
, dann würden alle Routen, die wir in routes.js
definieren, unter localhost:3000/api
verfügbar sein. Wenn wir dann also z.B. in der routes.js
die Zeile 5
zu
router.get('/fiw', async(req, res) => {
ändern, dann ist der GET-Endpunkt localhost:3000/api/fiw
.
MongoDB installieren¶
MongoDB ist die am meisten verwendete NoSQL (not only SQL) Datenbank. Sie basiert nicht auf Relationen, Tabellen und ihren Beziehungen zueinander (ist also keine relationale Datenbank), sondern speichert Dokumente in JSON-ähnlichem Format. Die Community Edition der MongoDB ist Open Source und kostenlos verfügbar. Wir verwenden hier jedoch eine Cloud-Instanz MongoDB Atlas. Um die Cloud-Version zu verwenden, müssen Sie sich bei MongoDB registrieren und einloggen. Wählen Sie dann einen kostenlosen Cluster. Diesen habe ich IKT-PWA
genannt:
Wenn Sie unter dieser Ansicht auf Connect
klicken und dann Drivers
, erscheint folgendes Fenster:
Der dort unter 3. aufgeführte connection string
ist für Sie wichtig, um sich mit der datenbank auf MongoDB Atlas zu verbinden. Hier habe ich zur Authentifizierung ein X.509-Zertifikat verwendet. Wenn Sie stattdessen ein Passwort gewählt haben, müssen Sie darin den String <password>
durch Ihr Passwort ersetzen, um sich mit der MongoDB zu verbinden. Wenn Sie MongoDB lokal installiert haben, ist der connection string
typischer Weise mongodb+://localhost:27017
.
MongoDB Compass¶
Um sich Ihre MongoDB-Datenbanken anzuschauen, empfehle ich Ihnen das Tool MongoDB Compass. Download und Installation sind normalerweise einfach. Stellen Sie mithilfe des connection strings
eine Verbindung zur MongoDB her (siehe z.B. hier).
Das Modul MongoDB installieren¶
Zur Verwendung von MongoDB im Backend verwenden wir als offiziellen MongoDB-Node-Treiber das Modul MongoDB. Wir installieren MongoDB mithilfe von
npm install mongodb
In die package.json
wird das Paket und die entsprechende Abhängigkeit eingetragen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
MongoDB stellt eine einfach zu verwendende Schnittstelle zwischen Node.js und MongoDB bereit. Bevor wir uns mit der MongoDB verbinden, erstellen wir zunächst noch eine Datenbank.
Dotenv für sichere Zugangsdaten¶
Für die "geheimen" Zugangsdaten (die jetzt noch gar nicht "geheim" sind) verwenden wir das dotenv-Paket:
npm install dotenv --save
Im Projektordner erstellen wir und eine Datei .env
(mit vorangestelltem Punkt!) und weisen darin dem Schlüssel DB_CONNECTION
eine Wert zu. Dieser Wert entspricht dem connection string
zu Ihrer MongoDB. D.h. für den Fall, dass Sie eine lokale Installation von MongoDB Community Server haben, könnte er wie folgt lauten (dabei ist htwinsta
bereits als Datenbankname angegeben!):
1 |
|
Beachten Sie, dass der Wert nicht in Hochkomma steht und dass auch kein Semikolon folgt!
Da ich die Authentifizierung mittels X.509-Zertifikat gewählt habe, sieht bei mir die .env
-Datei z.B. so aus:
1 2 3 4 |
|
Ich habe für den Namen der Datenbank ein eigenes Schlüssel-Wertepaar (DB_NAME = htwinsta
) angelegt und meinen Schlüssel in den Ordner assets
abgelegt, auf den ich dann mithilfe von PATH_TO_PEM
zugreife. Außerdem habe ich auch eine COLLECTION
definiert (posts
), in die dann die Datensätze geschrieben werden soll. Sie können sich darin z.B. auch den Port konfigurieren, auf dem Ihr Backend laufen soll. Beachten Sie, die .env
-Datei in die .gitignore
einzutragen. Die .env
-Datei sollte nicht committed werden!
db.js - Verbindung zur MongoDB¶
Zur Verwaltung der Verbindung zur MongoDB erstellen wir ein Skript db.js
im Ordner configure
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Hier sind mehrere Dinge erwähnenswert:
- Nach dem Aufruf der
config()
-Funktion vondotenv
(siehe Zeile2
) können wir mithilfe vonprocess.env.
auf die einzelnen Schlüssel bzw. deren Werte aus der.env
-Datei zugreifen. - Wenn Sie sich nicht mithilfe eines Zertifikates authentifizieren, dann entfallen alle Zeilen mit
credentials
. - Wir exportieren gleich mehrere Objekte. Dann kann wahlweise eines oder mehrere dieser Objekte in Skripte eingebunden werden, in denen diese jeweils benötigt werden.
CRUD-Zugriffe auf die Datenbank¶
Nun haben wir alles, was wir benötigen, um unsere Anfragen zu implementieren. Wir nutzen den express.Router
, um die Routen zu definieren und können mithilfe des db.js
-Skriptes auf MongoDB zugreifen. Wir werden nun sukzessive alle Anfragen in die routes/post.routes.js
einfügen.
R - read all¶
Wir beginnen mit der Anfrage, alle Daten aus der Datenbank auszulesen. Für die MongoDB erfolgt dies mit der Funktion find()
. In post.routes.js
ändern wir unsere GET
-Anfrage wie folgt:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Beachten Sie, dass wir dazu collection
aus der db.js
(Zeile 3
) verwenden. Diese gibt uns die Verbindung zur Collection posts
. Die Route wird mit localhost:3000/posts
definiert (siehe server.js
). Die anonyme Callback-Funktion enthält noch zwei Schlüsselwörter: async
und await
. Die Funktion find()
ist ein Promise (siehe dazu hier). Die Funktion find()
wird asynchron ausgeführt und "irgendwann" ist entweder das Ergebnis dieser Funktion verfügbar oder die Funktion gibt einen Fehler zurück. Auf eines der beiden wird gewartet (await
). Nur eine als async
deklarierte Funktion darf einen await
-Aufruf enthalten (siehe dazu z.B. hier).
neben dem Array aller Einträge in der posts
-Collection (Zeile 9
) wird auch der HTTP-Statuscode 200
zurückgesendet (Zeile 8
). Wenn Sie nun in Postman GET http://localhost:3000/posts
aufrufen, erscheinen alle Einträge aus der Datenbank. Allerdings haben wir dort noch keine Einträge. Wir bekommen deshalb ein leeres Array []
zurück.
C - create¶
Als nächstes implementieren wir einen Endpunkt, an dem wir einen neuen Datensatz in die Datenbank anlegen können. Dafür gibt es die http-Methode POST
. Wir führen also nicht mehr eine GET
-, sondern eine POST
-Anfrage durch. Bei dieser POST
-Anfrage wird der neue Datensatz an den Webserver mitgeschickt. Dies erfolgt im body
des request
-Objektes. Das Schreiben des Datensatzes in die Datenbank erfolgt mithilfe der save()
-Funktion von MongoDB.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
In den Zeilen 17-19
werden die Daten aus dem body
des request
-Objektes ausgelesen und mit diesen Daten ein neues Post
-Objekt erzeugt. Dieses neue Post
-Objekt (newPost
) wird in Zeile 21
in die Datenbank gespeichert und in Zeile 23
als response
zusammen mit der Statusmeldung 201
(created
) zurückgeschickt.
Nun geben wir in Postman POST http://localhost:3000/posts
ein und befüllen den Body
z.B. mit:
1 2 3 4 5 |
|
Achten Sie darauf, dass in der zweiten Menüzeile rechts JSON
ausgewählt ist (im Bild blau) - nicht Text
. Wir klicken auf Send
und es erscheint:
Schauen Sie auch in MongoDB Compass nach, ob der Datensatz dort erscheint:
R - read one¶
Wir erweitern die post.routes.js
um einen Endpunkt, der uns für eine gegebene id
den entsprechenden Datensatz zurückliefert. Die _id
werden von MongoDB automatisch vergeben und sind recht kryptisch, also z.B. "6475ba5e88a1b91688569dda"
(siehe oben). Wir können natürlich nach jedem beliebigen Wert für jeden Schlüssel in der Datenbank suchen. Wir nehmen hier beispielhaft die _id
, da die Suche nach einer _id
ein klein wenig komplexer ist, weil es sich dabei um eine ObjectId
handelt (im Gegensatz zu z.B. location
oder image_id
, welche reine Strings sind).
Die id
wird aus der URL des Endpunktes ausgelesen, d.h. wenn wir bspw. den Endpunkt GET http://localhost:3000/posts/6475ba5e88a1b91688569dda
eingeben, dann soll der Datensatz mit der _id: 6475ba5e88a1b91688569dda
im JSON-Format zurückgegeben werden. Wir nutzen dazu parametrisierte Routen und lesen die id
aus der Parameterliste aus. Paremtrisierte Routen werden per :
und dann den Namen des Parameters (hier id
) erstellt. Um dann den Wert des Parametrs id
aus der Parameterliste auszulesen, wird params
verwendet.
Da es sich bei der _id
um eine ObjectId
handelt (siehe oberes Bild von Compass), müssen wir diesen Typ zunächst aus dem mongodb
-Package importieren:
4 |
|
Wir nutzen die gleichnamige Variable ObjectId
. Nun können wir mithilfe von req.params
die id
auslesen, die der Endpunkt-URL angehängt wird (siehe '/:id'
in Zeile 34
):
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
Wir erzeugen mithilfe der id
ein neues ObjectId
-Objekt (Zeile 37
). Zum Finden der passenden Datensätze wird in MongoDB die Funktion find()
verwendet (siehe hier). Wird der Datensatz gefunden, d.h. existiert die entsprechende _id
, dann wird dieser in der response
zurückgesendet (Zeile 41
). Existiert er nicht, wird der HTTP-Statuscode 404
gesendet (Zeile 43
) und ein JSON mit der error
-Nachricht Post does not exist!
(Zeilen 44-46
).
Nun geben wir in Postman z.B. GET http://localhost:3000/posts/6475ba5e88a1b91688569dda
ein (bei Ihnen sind die _id
-Werte andere!) und erhalten:
Probieren Sie auch einmal GET http://localhost:3000/posts/0
aus, um die Fehlermeldung als JSON zu sehen.
U - update¶
Um einen bereits existierenden Datensatz zu ändern, kann entweder die HTTP-Anfrage PUT
oder PATCH
verwendet werden. Zur Unterscheidung zwischen PUT
und PATCH
siehe z.B. hier oder hier. Um einen Datensatz in der MongoDB zu ändern, stehen prinzipiell mehrere Funktionen zur Verfüging:
updateOne()
: ändert einzelne (oder alle) Teile eines Datensatzes und sendet die_id
zurück, falls ein neur Datensatz angelegt wurde,findOneAndUpdate()
: ändert einzelne (oder alle) Teile eines Datensatzes und sendet den kompletten Datensatz zurück,replaceOne()
: ändert den kompletten Datensatz.
In der folgenden Implementierung haben wir uns für die HTTP-Anfragemethode PATCH
und für die MongoDB-Funktion updateOne()
entschieden. Diese Funktion erwartet als ersten Parameter einen <filter>
, d.h. die Werte, nach denen nach einem Datensatz gesucht werden soll. Im folgenden Beispiel ist der Filter die _id
. Dazu wird erneute ein Parameter id
für die URL definiert. Der zweite Parameter der updateOne()
-Funktion sind die zu ändernden Werte für diesen Datensatz. Die zu ändernden Werte werden mithilfe von $set :
zur Änderung angegeben 8siehe Zeile 68
). In der folgenden Implementierung werden diese zu ändernden Werte als ein JSON dem body
des request
-Objektes übergeben. Um zu ermöglichen, dass ein, zwei oder drei Schlüssel-Werte-Paare in diesem JSON enthalten sein können, prüfen wir die Einträge im body
und setzen daraus ein neues post
-Objekt zusammen, wenn es bereits in der Datenbank existiert (deshalb zunächst findOne()
):
```javascript linenums="50"
// PATCH (update) one post router.patch('/:id', async(req, res) => { try { const id_obj = new ObjectId(req.params.id); const post = await collection.findOne({ _id: id_obj })
if (req.body.title) {
post.title = req.body.title
}
if (req.body.location) {
post.location = req.body.location
}
if (req.body.image_id) {
post.image_id = req.body.image_id
}
await collection.updateOne({ _id: id_obj }, { $set: post });
res.send(post)
} catch { res.status(404) res.send({ error: "Post does not exist!" }) }
}); ```
Wir können diese Funktion in Postman ausprobieren, indem wir im body
z.B. das JSON
1 2 3 |
|
mit unserem Request übergeben und PATCH http://localhost:3000/posts/6475ba5e88a1b91688569dda
wählen (bei Ihnen eine andere id
!). Der Datensatz mit der _id=6475ba5e88a1b91688569dda
wird dann aktualisiert.
Schauen Sie auch in der Datenbank nach (z.B. in MongoDB Compass) und wählen auch ruhig nochmal GET http://localhost:3000/posts
(z.B. in Postman).
D - delete one¶
Jetzt implementieren wir noch den Endpunkt, um einen Datensatz zu löschen. Dazu werden die HTTP-Anfragemethode DELETE
und die MongoDB-Funktion deleteOne()
verwendet. Im folgenden Beispiel wird der Datensatz erneut über die _id
ermittelt und dafür erneut die parametrisierte URL ausgelesen:
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
|
Die Rückgabe der deleteOne()
-Funktion enthält die Eigenschaft deletedCount
, die die Anzahl der gelöschten Datensätze enthält. Wir verwenden diese (Zeile 82
), um zu ermitteln, ob ein oder kein Datensatz gelöscht wurde. je nachden, wird der Statuscode 204
oder 404
zurückgegeben. Wenn wir nun in Postman z.B. DELETE http://localhost:3000/members/6475ba5e88a1b91688569dda
wählen (bei Ihnen eine andere id
!), wird der Datensatz mit der _id=6475ba5e88a1b91688569dda
aus der Datenbank gelöscht.
Hier nochmal die vollständige routes/post.routes.js
:
routes/post.routes.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
|
Cross-Origin Resource Sharing (CORS)¶
Die Same Origin Policy (SOP) ist ein Sicherheitskonzept, das clientseitig Skriptsprachen (also z.B. JavaScript oder CSS) untersagt, Ressourcen aus verschiedenen Herkunften zu verwenden, also von verschiedenen Servern. Dadurch soll verhindert werden, dass fremde Skripte in die bestehende Client-Server-Kommunikation eingeschleust werden. Gleiche Herkunft (origin) bedeutet, dass das gleiche Protokoll (z.B. http
oder https
), von der gleichen Domain (z.B. localhost
oder htw-berlin
) sowie dem gleichen Port (z.B. 80
oder 4200
) verwendet werden. Es müssen alle drei Eigenschaften übereinstimmen.
Mit dem Aufkommen von Single Page Applications und dem darin benötigten AJAX kam jedoch der Bedarf auf, die SOP aufzuweichen. Es sollte möglich sein, dass z.B. JavaScript sowohl client-seitig das DOM ändert als auch einen Request an den Server (das Backend) sendet. Der Kompromiss, der dafür gefunden wurde, nennt sich Cross-Origin Resource Sharing (CORS). Damit ist es möglich, für einige oder alle Anfragen zu definieren, dass sie im Sinne der SOP trotzdem erlaub sein sollen.
Um CORS für Ihr Backend zu aktivieren, wechseln Sie im Terminal in Ihren backend
-Ordner und geben dort
npm install cors
ein. Öffnen Sie dann die server.js
und fügen Sie die hervorgehobenen Zeilen ein:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Wenn Sie z.B. nur die get
-Anfrage teilen wollen, dann wählen Sie nicht app.use(cors());
, sondern
app.get("/", cors(), (req, res) => {
res.json({ message: "Hello FIW!" });
});
Mehr zum CORS-Paket von node.js bzw. express finden Sie hier.
Success
Das bis hier erstellte Backend ist unter https://github.com/jfreiheit/IKT-PWA-Backend.git verfügbar.