Technische Artikel

Node.js Service schnell mit Koa und TypeScript erstellen

node js service with koa

Für die Implementierung eines Node.js Back-End Service kannst du unterschiedliche Frameworks oder reines HTTP verwenden. Heute schauen wir uns einmal an, wie das ganze mit Koa funktioniert.

Folgende Themen gehen wir gemeinsam durch:

Koa

Koa ist ein Web Framework von den Machern von Express. Es unterscheidet sich dabei durch eine kleineres, ausdrucksvolleres und robusteres Fundament. Dieses wird für Web Applikationen und APIs verwendet. Somit können Server schnell und einfach geschrieben werden.

Bevor wir starten

Für diesen Artikel benötigst du eine bereits laufende Node.js Umgebung. Du solltest dein Setup soweit vorbereitet haben, um eine index.ts Datei mit node auszugeben. Zudem werden wir unsere Implementierung später mit Postman testen.

Wenn du dir unsicher über das Setup bist, kannst du dir gerne mein GitHub Repository Skeleton-Nodejs-Setup anschauen.

Du kannst auch einen Blick auf meine Package.json werfen, mit der ich starten werde:

Und dies ist meine Ordnerstruktur:

Konzeption

Vor der Implementierung ist eine gut durchdachte Konzeption wichtig. Dies erleichtert dir später das Schreiben deines Codes. Zudem unterstützt die Konzeption dich dabei, vorab Herausforderungen zu erkennen und anzugehen.

Die Idee

Unser Node.js Service ist Teil einer umfangreichen ToDo-Applikation, bestehend aus Client, Back-End Service und Datenbank. Wir kümmern uns heute um den Service. Dieser dient als Schnittstelle zwischen dem Front-End und der Datenbank.

Das Ziel

Generell soll unser Service HTTP-Anfragen vom Client entgegennehmen, verarbeiten und entsprechende Antworten senden können. Hierbei handelt es sich um die CRUD Operationen, also Create, Read, Update und Delete. Um dies zu bewerkstelligen, erstellen wir die folgenden HTTP-Endpunkte: GET, POST, PUT und DELETE. 

Out of Scope

Die Anbindung an eine Datenbank sprengt den Rahmen dieses Artikels. Aus diesem Grund werden wir alle Antworten simulieren. Die Endpunkte werden nach unsere Implementierung aber soweit funktionsfähig sein, dass sie im nächsten Schritt an eine Datenbank angebunden werden können.

Implementierung

Da wir nun ein gutes Bild über die Systemarchitektur haben und das Ziel unseres Service kennen, können wir nun mit der Programmierung beginnen.

Schritt 1: Server zum Laufen bringen

Um unseren Service funktionstüchtig zu bekommen, starten wir zuerst den Server. Hierfür benötigen wir einige Packages.

Packages installieren

Zuerst fügen wir koa, @koa/router und koa-bodyparser zu unsere Package.json hinzu. Dies machen wir mit dem Befehl:

yarn add koa @koa/router koa-bodyparser

Danach installieren wir die Koa-TypeScript Packages als DevDependencies:

yarn add -D @types/koa @types/koa-bodyparser @types/koa__router
Server erstellen

Als nächstes erstellen wir einen Server-Ordner, der die Datei koa.server.ts enthält:

mkdir src/server && touch src/server/koa.server.ts
koa.server.ts

In der koa.server.ts Datei instanziieren wir einen neuen Koa Server und übergeben diesen der Variable app. Zudem erstellen wir eine Variable port, der wir 4000 als Zahl übergeben.
In der runServer Funktion horcht unser Server auf diesen Port. 

index.ts

Damit die runServer Funktion ausgeführt wird, importieren wir sie in die index.ts Datei.

Server starten

Und schon können wir unseren soeben implementierten Server über das Terminal starten. Mit yarn dev fährt der Server hoch. Hat alles geklappt, erhalten wir die Rückmeldung “server is listening on port 4000”.

Um automatisch auf zukünftige Änderungen zu reagieren, nutzen wir yarn watch in einem zweiten Terminal-Tab. So umgehen wir das regelmäßige beenden und neu starten des Servers.

Hier siehst du meine Konsolenausgabe nach der Ausführung von yarn dev und yarn watch:

Schritt 2: GET Endpunkt anlegen

Jetzt wird es Zeit unseren ersten Endpunkt zu erstellen. Dieser GET Endpunkt sorgt dafür, dass der Server von außen erreichbar ist. Wir können dadurch eine Anfrage stellen und eine Antwort erhalten.

Ordner und Dateien anlegen

Wir benötigen weitere Ordner, die wir mit dem folgenden Befehl erstellen:

mkdir src/middleware src/service src/routes src/utils

Die dazugehörigen Dateien erstellen wir mit:

touch src/service/todo.service.ts src/middleware/todo.middleware.ts src/routes/todo.routes.ts src/utils/interfaces.ts
interface.ts

Wir beginnen mit der interface.ts Datei. Die Antwort die wir bei unserer GET Anfrage erhalten wollen, ist eine Liste an ToDos. Hierfür benötigen wir ein ToDo Interface.

Zum aktuellen Zeitpunkt soll ein ToDo aus einer id sowie einem task vom Typ string bestehen. Das Interface können wir später erweitern, wenn wir noch weitere Properties benötigen.

Exkurs Koa Middleware

Eine Koa Middleware ist im Wesentlichen eine Generatorfunktion. Generatoren sind Funktionsausführungen, die unterbrochen und zu einem späteren Zeitpunkt wieder aufgenommen werden können. Im Fall von Koa gibt eine Generatorenfunktion eine weitere Generatorenfunktion zurück und nimmt eine andere an.

Dies geschieht nach dem Zwiebel Prinzip. Die äußerste Middleware gibt die nachgelagerte Middleware zurück. Das passiert solange, bis die innerste Middleware erreicht ist. Danach dreht sich die Richtung um. Nun ruft die innerste Middleware die vorgelagerte auf bis die äußerste erreicht ist. 

Dadurch kann alles, was im Request und Response-Objekt enthalten ist, von den Middlewares angenommen und verarbeitet werden. Sowohl der Request wie auch der Response sind dabei im Koa Context enthalten. Hierzu gleich mehr.

  • next()

Mit der Funktion next() aus dem Koa Modul können wir die nachgelagerte Middleware aufrufen. Wir werden diese später in unserer todo.middleware.ts benötigen.

  • koa.use()

Um die äußerste Middleware zu generieren, dient koa.use(). In unserem Fall allerdings app.use(), da wir der Variablen app eine neue Instanz von Koa hinzugefügt haben. Dies werden wir gleich in unserer koa.server.ts anpassen.

todo.router.ts

Unsere Routing Funktion ist auch eine Koa Middleware. Wir können sie selbst schreiben oder ein vorhandenes Modul wie @koa/router nutzen. Da wir uns für die zweite Variante entscheiden, importieren wir @koa/router in todo.router.ts. Nun erstellen wir eine neue Router Instanz und übergeben sie der Variable router.

Jetzt benötigen wir eine todoRoutes Funktion, die wir exportieren können. Innerhalb von todoRoutes werden wir alle Routen bündeln, die wir für unsere CRUD Operationen benötigen.

  • router.prefix()

Damit wir nicht bei allen Endpunkten ‚/todo‘ für den Pfad schreiben müssen, nutzen wir router.prefix(). Dadurch erhalten alle nachgelagerten Endpunkte automatisch diesen Pfad. Dies wird insbesondere bei der Implementierung von POST, PUT und DELETE relevant.

  • router.get()

Unser GET Endpunkt erhält neben dem Pfadparameter eine Middleware. Um jedoch unsere Datei übersichtlich zu halten, lagern wird diese als getAll in todo.middleware.ts aus.

  • router.routes()

Letztendlich geben wir router.routes() zurück. Dies ist eine weitere Middleware, die eine der Anfrage entsprechende Route versendet. Router.routes() wird erst ausgeführt, sobald die Middleware in getAll fertig ist. Wir erinnern uns an das Zwiebel Prinzip.

todo.middlware.ts

In der todo.middleware.ts deklarieren wir eine getAll Funktion. Wie oben erwähnt, handelt es sich dabei um eine Middleware. Die zugehörige Typisierung importieren wir aus dem Koa Modul zusammen mit Context und Next.

  • ctx Parameter

Der ctx Parameter enthält unter anderem die Request- und Response-Objekte, die wir für die weitere Verarbeitung nutzen werden. Dieser ist mit Context typisiert.

  • next Parameter

Der next Parameter dient uns zum Aufrufen der nächsten Middleware. Ihm ordnen wir den Typ Next zu.

Innerhalb der getAll Funktion übergeben wir dem Response Body unsere ToDo Liste. Wir erhalten sie beim Aufruf auf findAll, die wir in der todo.service.ts deklarieren werden. 

Zum Schluss geben wir next() zurück. Der Rückgabewert von next() ist mit Promise<any> typisiert, was wir für getAll übernehmen können. Wir ersetzten lediglich Promise<any> durch Promise<ToDo[]>. Und sind so mit der Typisierung der Funktion fertig. 

todo.service.ts

In der todo.service.ts würden wir eigentlich die Anfrage an die Datenbank stellen. Da dies den Umfang des Artikels übersteigt, werden wir die Antwort der Datenbank simulieren.

  • Simulation der Datenbank-Antwort

Hierfür benötigen wir eine Liste an Arrays. Wir definieren deshalb die Variable toDoArray vom Typ ToDo[]. Das toDoArray enthält zwei toDo Objekte.

Als nächstes deklarieren wir unsere findAll Funktion, mit dem Typ ToDo[] als Rückgabewert. Innerhalb von findAll legen wir die Variable toDos an und ordnen ihr toDoArray zu.

Zum Schluss wird toDos ausgegeben.

Importe anpassen

Wenn du die Schritte von oben nach unten befolgt hast, musst du gegebenenfalls die Importe in den einzelnen Dateien anpassen. Je nachdem welche IDE du verwendest, hast du eventuell bereits Fehlermeldungen erhalten.

koa.server.ts ergänzen

Damit die Middlewares aufgerufen werden, müssen wir noch eine kleine Änderung in der koa.server.ts vornehmen. Wir importieren die todoRoutes Funktion aus unserer todo.routes.ts.

Wie oben bereits angedeutet, benötigen wir nun app.use(). Dadurch rufen wir die äußerste Middleware auf und können so alle anderen Middlewares durchlaufen lassen.

Mit Postman testen

Und das war es eigentlich auch schon. Wir können unseren GET Endpunkt mit Postman testen.

Hierfür öffnen wir einen neuen Tab mit der localhost:4000/todo URL und klicken auf senden. Wenn alles geklappt hat, siehst du nun den Response Body auf der rechten Seite.

Schritt 3: POST Endpunkt anlegen

Nun wollen wir unseren zweiten Endpunkt erstellen. Innerhalb der POST Anfrage, werden wir einen Request Body mitsenden. Der Service nimmt diesen an, verarbeitet ihn und gibt eine Antwort zurück.

koa.server.ts ergänzen

Da wir in unserem POST Request einen Body mit dem neuen ToDo schicken werden, benötigen wir die Middleware koa-bodyparser. Diese übergeben wir als Parameter an app.use() in unserer koa.server.ts. Da sie die erste Middleware ist die aufgerufen werden soll, platzieren wir sie vor app.use(todoRoutes()).

todo.router.ts

Innerhalb von todo.routes.ts erstellen wir einen weiteren Endpunkt mit router.post(). Als Pfadparameter erhält auch dieser ‘/‘.

Die Middleware zur Verarbeitung der Anfrage lagern wir ähnlich wie bei GET als Funktion aus. Diese deklarieren wir als createToDo.

todo.middlware.ts

Unsere Funktion createToDo wird in todo.middleware deklariert und exportiert. Es handelt sich wie bei getAll um eine Funktion vom Typ Middleware. Auch sie bekommt ctx und next als Parameter übergeben. Der Rückgabewert wird als Promise<ToDo> typisiert.

  • Variable newTodo

Innerhalb von createToDo legen wir eine Variable mit dem Namen newTodo vom Typ ToDo an. Dieser weisen wir den Request Body zu.

Dem Response Body übergeben wir die Informationen, die wir aus der insertOne Methode erhalten. Diese deklarieren wir in todo.service.ts. Die insertOne Methode nimmt einen Parameter an. Hierbei handelt es sich um die Variable newTodo, der wir den Request Body zugeordnet hatten.

Und zum Schluss geben wir next() zurück.

todo.service.ts

Als letzten Schritt passen wir unsere todo.service.ts an. Hier deklarieren wir eine insertOne Methode und exportieren sie. Die insertOne Methode nimmt einen Parameter an, den wir mit ToDo typisieren. Zudem gibt sie einen Wert vom Typ ToDo zurück.

  • Variable toDos

Damit wir eine Kopie des toDoArrays erhalten, erstellen wir eine neue Variable toDos vom Typ ToDo[]. Ihr übergeben wir toDoArray. 

Der Parameter todo wird mit push() zu toDos hinzugefügt.

Damit unser toDoArray zukünftig auch die neu erstellten todos enthalten, überschreiben wir sie zum Schluss mit toDos. Um dies machen zu können, müssen wir in Zeile 3 const toDoArray in let toDoArray ändern.

Als Ausgabewert geben wir den Parameter todo zurück.

Mit Postman testen

Jetzt können wir unseren POST Endpunkt mit Postman testen. Wir öffnen einen neuen Tab, ändern die Anfrage in POST und geben die URL ein. Der Request Body ist im JSON Format und sieht bei mir wie folgt aus:

Nach dem Klicken auf senden, erhalten wir einen Statuscode 200 und den Response Body zurück.

Schritt 4: PUT Endpunkt anlegen

Weiter geht es mit unserem dritten Endpunkt. Innerhalb der PUT Anfrage werden wir einen Request Body mitsenden, ähnlich wie bei POST. Dabei werden wir gezielt ein todo ersetzen und eine Antwort senden.

todo.router.ts

Wir erstellen in todo.routes.ts einen weiteren Endpunkt mit router.put(). Als Pfadparameter erhält er ‘/:id’. Dies ist wichtig, damit wir später ein bestimmtes todo Objekt anhand der ID finden und verändert können.

Die Middleware zur Verarbeitung der Anfrage lagern wir als updateToDo Funktion aus.

todo.middlware.ts

Wie bei den anderen Endpunkten, deklarieren wir auch dieses Mal unsere Funktion updateToDo in todo.middleware.ts. Der Typ Middleware sowie die Parameter ctx und next bleiben auch hier unverändert. Der Rückgabewert wird als Promise<ToDo> typisiert.

  • Variablen todo und id

Innerhalb von updateToDo benötigen wir nun zwei Variablen. Diese geben wir als Parameter weiter. Ähnlich wie bei createToDo, erzeugen wir eine Variable todo mit dem Wert Request Body.

Zusätzlich benötigen wir eine Variable id mit der Typisierung string. Diese erhält den Pfadparameter id, den wir bei der Anfrage in der URL mitschicken.

Die soeben erstellten Parameter werden an die updateOneById Funktion übergeben, die wir in todo.service.ts deklarieren werden.

Zum Schluss geben wir next() zurück.

todo.service.ts

Als letzten Schritt passen wir unsere todo.service.ts an. Hier deklarieren und exportieren wir updateOneById. Diese Funktion nimmt zwei Parameter entgegen: id vom Typ string und todo vom Typ ToDo. Der Rückgabewert ist mit ToDo typisiert.

Innerhalb von updateOneById erzeugen wir ein leeres Array vom Typ ToDo[]. Wir nennen es newToDoArray. Ähnlich wie bei insertOne, erstellen wir zudem eine Kopie des toDoArrays.

  • forEach

Um nach der Anpassung des todo Objekts die gleiche Reihenfolge im Array beizubehalten, werden wir mit einer forEach Schleife arbeiten. Wir schauen uns dabei jedes einzelne Element des Arrays an. Dabei vergleichen wir die id innerhalb des Elements mit der id, die wir als Parameter übergeben haben.

  • it.id !== id

Wenn die id des Elements ungleich der Parameter id ist, wird das Element zum newToDoArray mittels push() hinzugefügt.

  • it.id === id

Stimmt die id des Elements mit der Parameter id überein, wird der Parameter todo zum newToDoArray hinzugefügt. Das eigentliche Element wird so mit dem neuen todo ersetzt.

  • Hintergrund

Warum machen wir das? Indem wir ein neues Array mit den Elementen und dem Parameter todo befüllen, behalten wir die ursprüngliche Sortierung bei. Ansonsten kann es zum Beispiel dazu kommen, dass das todo mit der id 1 von der ersten auf die letzte Stelle in unserer Liste wandert. 

Nun überschreiben wir das ursprüngliche toDoArray mit newToDoArray. Wir haben also unser geändertes todo Objekt zu unserer Liste gefügt und dabei die ursprüngliche Reihenfolge beibehalten.

Den Parameter todo geben wir am Ende als Ausgabewert zurück.

Mit Postman testen

Jetzt können wir unseren PUT Endpunkt mit Postman testen. Hierfür ändern wir den Task im ersten todo. Nach dem Klick auf senden, erhalten wir den Statuscode 200 und unseren Response Body.

Sobald wir eine weitere GET Anfrage durchgeführt haben, sehen wir die Änderung im ersten Objekt.

Schritt 5: DELETE Endpunkt anlegen

Kommen wir zu unserem letzten Endpunkt. Mit der DELETE Anfrage werden wir ein bestimmtes todo löschen. Der Service wird die Anfrage annehmen, verarbeiten und eine entsprechende Antwort senden.

todo.router.ts

In todo.router.ts erstellen wir einen weiteren Endpunkt mit router.del(). Auch dieser bekommt den Pfadparameter ‘/:id’. Ähnlich wie bei PUT, können wir damit ein bestimmtes todo Objekt finden und entfernen.

Die Middleware zur Verarbeitung der Anfrage lagern wir als deleteToDo Funktion aus.

todo.middlware.ts

Wie bei den anderen Endpunkten, deklarieren wir unsere Funktion deleteToDo in todo.middleware.ts. Der Typ Middleware sowie die Parameter ctx und next bleiben auch hier unverändert. Da es keinen Rückgabewert geben wird, typisieren wir ihn mit Promise<void>.

  • Variable id

Innerhalb von deleteToDo erzeugen wir eine Variable id vom Typ string. Diese enthält den Pfadparameter id, den wir bei der Anfrage in der URL mitschicken.

Der Parameter id wird an die deleteOneById Funktion übergeben, die wir in todo.service.ts deklarieren werden.

Zum Schluss geben wir next() zurück.

todo.service.ts

Als letzten Schritt passen wir unsere todo.service.ts an. Hier deklarieren und exportieren wir deleteOneById. Diese Funktion nimmt einen Parameter entgegen: id vom Typ string. Der Rückgabewert ist mit void typisiert, da die Funktion keinen Wert zurück gibt.

Ähnlich wie bei updateOneById, erzeugen wir in deleteOneById ein leeres Array vom Typ ToDo[]. Wir nennen es newToDoArray. Zudem erstellen wir eine Kopie des toDoArrays.

  • forEach

Wir nutzen wieder eine forEach Schleife. Wir schauen uns jedes einzelne Element des Arrays an. Dabei vergleichen wir die id innerhalb des Elements mit der id, die wir als Parameter übergeben haben.

  • it.id !== id

Wenn die id des Elements ungleich der Parameter id ist, wird das Element zum newToDoArray mittels push() hinzugefügt.

  • it.id === id

Stimmt die id des Elements mit der Parameter id überein, passiert gar nichts. Da es sich hier um das zu löschende todo handelt, benötigen wir es nicht in newToDoArray. Aus diesem Grund müssen wir auch keinen else Fall schreiben, den es ist keine Aktion notwendig.

Nun überschreiben wir das ursprüngliche toDoArray mit newToDoArray. 

Mit Postman testen

Jetzt sind wir mit unserer Implementierung komplett fertig. Wir können sie also testen und damit spielen.

Wenn wir eine Delete Anfrage auf eine bestimmte id stellen, erhalten wir als Response den Statuscode 204 für No Content.

Wie du im zweiten Bild siehst, habe ich einige neue ToDos über den POST Request angelegt. Zudem habe ich mit dem PUT Request das erste ToDo angepasst und das zweite ToDo mit dem DELETE Call gelöscht. Zum Schluss habe ich mir alle ToDos mit GET ausgeben lassen.

Was noch zu tun wäre

In diesem Artikel haben wir Unit Tests und das komplette Error Handling außen vorgelassen. Wenn du also einen vollständigen Service bauen willst, gibt es vor dem Deployment noch einiges zu tun. Vielleicht fällt dir sogar noch mehr ein.

Zusammenfassung

Du hast jetzt gesehen, wie du einen Node.js Service mit Koa und TypeScript erstellen kannst.

Nutze gerne die Möglichkeit, deine Implementierung zu erweitern. Vielleicht kannst du den Service um weitere Endpunkte erweitern? Möglich sind hier zum Beispiel eine GET Anfrage, bei der du ein bestimmtest ToDo aufgrund der ID erhältst.

Oder du kannst einen PATCH Endpunkt anlegen, bei dem nur ein Teil des ToDo Objekts geändert und mitgeschickt wird. Hierbei müsstest du das ToDo Interface erweitern. Wie wäre es etwa mit einem Deadline- oder einem Prioritäts-Property innerhalb deines ToDo Objekts? Sei hier ganz kreativ und probiert dich aus.

Der nächste Schritt wäre nun, deinen Service mit einer Datenbank zu verbinden.

Quelle Hintergrund des Titelbilds: kostenlose Hintergrundfotos von .pngtree.com

Was dir auch gefallen könnte...

WordPress Cookie Hinweis von Real Cookie Banner