Deno   2. Teil: Babyschritte

Deno   2. Teil: Babyschritte

  • Beitrags-Autor:Thanh L
  • Beitrags-Kommentare:0 Kommentare

Mittlerweile ist Deno 2 Jahre alt. Das richtige Alter um mit ihm ein paar Schritte zu gehen. Im vorherigen Artikel habe ich einen Überblick gegeben, warum es erzeugt wurde, was Deno bereits kann und die Vor- und Nachteile diskutiert. In diesem Artikel will ich zusammen mit euch ein paar Gehversuche mit Deno unternehmen. Wir werden dazu ein kleines Projekt Schritt für Schritt aufbauen.

Ziel des Projektes wird es sein, eine simple CRUD REST API zu schreiben, mit der unsere Finanzberater Anträge ihrer Kunden verwalten können, in dem sie Anträge erstellen, löschen und Notizen zu den Anträgen hinzufügen können. Als http Server werde ich oak verwenden — das Pendant zum Express in Node. Über Postman werden wir unsere Applikation testen.

Das Aufsetzen mache ich auf einer Linux Maschine (Ubuntu 20.14 LTS). Die Zielsprache ist TypeScript (TS). Für das Editieren benutze ich VIM. Aber ihr könnt natürlich jeden anderen Editor benutzen.

Für die Ungeduldigen unter euch gibt es das gesamte Projekt als Git Repo unten zu finden.

Schritt 1: Installation und Setup

Zur Installation öffnet ihr das Terminal und gebt folgenden Befehl ein:

curl -fsSL https://deno.land/x/install/install.sh | sh

Als nächstes müsst ihr den Pfad in bashrc eintragen. Dazu bashrc öffnen

vim ~/.bashrc

und folgende Zeilen am besten ganz unten einfügen:

export DENO_INSTALL=”/home/your-computer/.deno”
export PATH=”$DENO_INSTALL/bin:$PATH”

Dann das Terminal neu starten. Um zu überprüfen, ob die Installation erfolgreich war, folgendes in das Terminal eintippen

deno --version

Bei mir bekomme ich folgende Ausgabe. Abhängig davon, wann ihr Deno installiert habt, schaut es bei euch entsprechend anders aus:

Für Hilfe könnt ihr diesen Command benutzen:

deno --help

Bevor ihr richtig anfangt, testet ihr erstmal aus, ob Deno richtig funktioniert. Dies könnt ihr auch ohne eine einzige, eigene Zeile Code zu schreiben, indem ihr folgenden Befehl in euren Terminal eingebt:

deno run https://deno.land/x/interhyp@0.0.1/interhyp.ts

Ihr ladet damit ein externes Modul herunter und führt es aus. Das erste, das ihr bemerken werdet, ist, dass Deno das Modul herunterlädt und dann einen Check ausführt:

Danach solltet euch Deno hiermit begrüßen:

Ihr werdet noch etwas anderes bemerken, wenn ihr den Befehl ein weiteres Mal eingebt:

deno run https://deno.land/x/interhyp@0.0.1/interhyp.ts

Ihr werdet dann sehen, dass die Schritte Download und Check entfallen sind. Der Grund, warum das so ist, werde ich weiter unten erklären. Ihr könnt aber mit cache --reload das Herunterladen auch erzwingen. In unserem Fall also:

deno cache --reload https://deno.land/x/interhyp@0.0.1/interhyp.ts

Wenn ihr bis zu diesem Punkt gekommen seid, dann könnt ihr davon ausgehen, dass Deno richtig installiert ist und funktioniert.

Schritt 2: Start

Die Ordnerstruktur unseres Projektes wollen wir ganz einfach halten und besteht aus folgenden Dateien:

.
├── app.ts
├── CaseItem.ts
├── controller.ts
├── db.ts
└── routes.ts

 Dazu erstellt ihr einen Ordner und geht in ihn rein:

mkdir deno-simple-api
cd deno-simple-api

Als nächstes erstellt ihr euer erstes File und solltet es gleich bearbeiten:

touch app.ts
vim app.ts

Ihr schreibt folgende Zeilen in die app.ts:

import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
const router = new Router();
const port: number = 8080;

router.get('/', ({ response }: { response: any }) => {
        response.body = {
        message: 'Hallo Interhyp!'
        };
});
app.use(router.routes());

app.addEventListener('listen', ({ secure, hostname, port }) => {
  const protocol = secure ? 'https://' : 'http://';
  const url = `${protocol}${hostname ?? 'localhost'}:${port}`;
  console.info(
  `Listening on ${url}
  );
});

await app.listen({ port });

Dem aufmerksamen Leser wird wahrscheinlich nicht entgangen sein, dass die Einrückung nicht standardkonform ist und ich Anführungsstriche inkonsistent verwendet habe. Statt nun dieses (oder mehrere) Files zu öffnen und einzeln zu korrigieren, bietet Deno einen Formatierer an. Mit folgendem Befehl kann ich die kritischen Stellen begutachten:

deno fmt --check

Wie unten zu sehen ist, zeigt mir Deno, an welcher Zeile ich welche Formatierungsfehler gemacht habe:

Statt sie nun einzeln zu berichtigen, brauche ich nur folgenden Befehl auszuführen und Deno korrigiert mir die ganze Datei:

deno fmt app.ts

Möchte ich mehrere Dateien von Deno korrigieren lassen, kann ich entweder eine weitere Datei anhängen:

deno fmt app.ts andereDatei.ts

oder einfach deno fmt schreiben und es korrigiert mir alle Dateien im aktuellen Pfad. Das Ergebnis der Formatierung schaut nun so aus:

import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
const router = new Router();
const port: number = 8080;

router.get("/", ({ response }: { response: any }) => {
   response.body = {
     message: "Hallo Interhyp!",
   };
});

app.use(router.routes());
app.addEventListener("listen", ({ secure, hostname, port }) => {
   const protocol = secure ? "https://" : "http://";
   const url = `${protocol}${hostname ?? "localhost"}:${port}`;
   console.info(
     `Listening on ${url}`
   );
});

await app.listen({ port });

Nice! Damit haben wir unseren ersten Server hinbekommen. Nachfolgend erkläre ich, was der obige Code macht. 

import { Application, Router } from "https://deno.land/x/oak/mod.ts";

Wie bereits zu Anfang schon angedeutet, als wir interhyp.ts heruntergeladen und ausgeführt haben, können wir Module über URLs importieren. Was in der obigen Zeile passiert ist: Deno schaut nach, ob es das Modul unter der URL schon hat. Falls nicht, dann geht es auf die URL, hier https://deno.land/x/oak/mod.ts , und lädt das Package herunter und cached es für einen späteren Gebrauch. Das liegt daran, dass Deno das heruntergeladene Modul in einen Cache speichert und beim nächsten Ausführen auf diesen Cache zurückgreift und dadurch nicht nur schneller ist, sondern mit dem Modul auch arbeiten kann, wenn man offline ist.

Der Rest dürfte allen vertraut sein, die schon mal mit einen http Server, wie z.B. Express gearbeitet haben: Wir haben eine GET Route, die den Text Halo Interhyp! zurückgibt, falls man den Pfad / im Client aufruft.

Wir werden die Route später noch verschieben. Aber zur Zeit soll sie erstmal da bleiben. 

Testen wir unsere Applikation, indem wir es wie folgt ausführen:

deno run app.ts

Statt dass die Applikation startet, bekommen wir folgende Fehlermeldung:

Check file:///home/think/dev/deno/deno-simple-api/app.ts
error: Uncaught PermissionDenied: network access to “0.0.0.0:8080”, run again with the — allow-net flag
 at processResponse (core.js:226:13)
 at Object.jsonOpSync (core.js:250:12)
 at opListen (deno:cli/rt/30_net.js:32:17)
 at Object.listen (deno:cli/rt/30_net.js:207:17)
 at Application.serve (server.ts:287:25)
 at Application.listen (application.ts:362:20)
 at app.ts:18:11

Dies ist eines von Denos Sicherheitsfeatures: Der Zugriff auf wichtige Bestandteile des Hostsystems, z.B. das Netzwerk, ist defaultmäßig beschränkt. Anders als bei Node muss man Deno explizit die Erlaubnis erteilen. Eine Auswahl möglicher Flags:

- allow-write # Schreibzugriff
- allow-read # Lesezugriff
- allow-network # Netzwerkzugriff
- allow-env # Zugriff auf Umgebungsvariablen
- a # Erlaube Zugriff auf alles

Die vollständige Liste ist hier zu finden.

Wir lassen die Applikation noch mal laufen — diesmal mit dem Flag:

deno run --allow-net app.ts

Wir testen nun unsere Applikation mit Postman:

Unsere Applikation funktioniert! Der Rest sollte ein Kinderspiel sein.

Schritt 3: Model

Erstellen wir nun die restlichen Dateien. Wir setzen mit CaseItem.ts fort. CaseItem ist ein Interface für einen Antrag. Der Antrag soll eine id und ein customer Feld für den Kunden enthalten, des Weiteren product für das Immobilienprodukt. Als letztes notes für Notizen, wo der Finanzberater Anmerkungen hinzufügen kann. 

export default interface CaseItem {
   id: string;
   customer: string;
   product: string;
   notes: string | null;
};

Schritt 4: Datenbank

Als nächstes erstellen wir für unsere Zwecke eine “Datenbank”, db.ts , in der wir unsere Änderungen an den CaseItem speichern, ohne eine richtige Datenbank anbinden zu müssen. Wir tragen schon ein paar Dummy-Werte ein, damit wir die Funktionalitäten unserer Applikation testen können:

import CaseItem from './CaseItem.ts'

const db: Array<CaseItem> = [{
   id: '1',
   customer: 'Ryan Dahl',
   product: 'Condo',
   notes: null
},{
   id: '2',
   customer: 'Brendan Eich',
   product: 'Haus mit Garten',
   notes: null
},{
   id: '3',
   customer: 'Anders Hejlsberg',
   product: 'Ferienwohnung',
   notes: null
}];

export default db;

Schritt 5: Controller

Im controller.ts implementieren wir die verschiedenen Funktionen der Applikation, wie getCaseItems , um alle Anträge zu beschaffen. getCaseItem , um einen Antrag anhand dessen id zu bekommen. addCaseItem , um einen Antrag hinzuzufügen. updateCaseItem , um einen Antrag zu aktualisieren. Mit deleteCaseitemkönnen wir unsere Anträge wieder löschen. Und dann noch eine Hilfsfunktion searchCaseItemById , die uns hilft, einen Antrag anhand dessen id zu finden.

import CaseItem from './CaseItem.ts'
import db from './db.ts'

let caseItems = db;

/**
* @description Get all cases
* @route GET /cases
*/
const getCaseItems = ({ response }: { response: any }) => {
   response.status = 200;
   response.body = caseItems;
};

/**
* @description Get a case by Id
* @route GET /cases/:id
*/
const getCaseItem = ({ params, response }: { params: { id: string }; response: any }) => {
   const caseItem: CaseItem | undefined = searchCaseItemById(params.id);
   if (caseItem) {
      response.status = 200;
      response.body = caseItem;
   } else {
      response.status = 404;
      response.body = { message: 'Case not found.' };
   }
};

/**
* @description Add a new case
* @route POST /cases
*/
const addCaseItem = async ({ request, response }: { request: any; response: any }) => {
   const body = await request.body();
   const caseItem: CaseItem = await body.value;
   caseItems.push(caseItem);
   response.body = { message: 'OK' };
   response.status = 200;
};

/**
* @description Update a case by Id
* @route PUT /cases/:id
*/
const updateCaseItem = async ({ params, request, response }: { params: { id: string }; request: any; response: any }) => {
   let caseItem: CaseItem | undefined = searchCaseItemById(params.id);
   if (caseItem) {
      const body = await request.body();
      const updateInfos: { customer?: string; product?: string; notes?: string } = await body.value;
      caseItem = { ...caseItem, ...updateInfos};
      caseItems = [...caseItems.filter(caseItem => caseItem.id !== params.id), caseItem];
      response.status = 200;
      response.body = { message: 'OK' };
   } else {
      response.status = 404;
      response.body = { message: `Case not found` };
   }
};

/**
* @description Delete a case by id
* @route DELETE /cases/:id
*/
const deleteCaseItem = ({ params, response }: { params: { id: string }; response: any }) => {
   caseItems = caseItems.filter(caseItem => caseItem.id !== params.id);
   response.body = { message: 'OK' };
   response.status = 200;
};

/**
* @description return the case if found and undefined if not
*/
const searchCaseItemById = (id: string): ( CaseItem | undefined ) => caseItems.filter(caseItem => caseItem.id === id )[0];

export { getCaseItems, addCaseItem, getCaseItem, updateCaseItem, deleteCaseItem };

Schritt 6: Router

Wir erstellen jetzt noch die routes.ts . Er dient dazu, den jeweiligen Pfaden die richtigen Funktionen zuzuordnen. Wir erstellen also für alle unsere Funktionen, die wir in unserercontroller.ts definiert haben, einen Pfad.

import { Router } from 'https://deno.land/x/oak/mod.ts';
import { getCaseItems, addCaseItem, getCaseItem, updateCaseItem, deleteCaseItem } from './controller.ts';

const router = new Router();

router
.get('/cases', getCaseItems)
.post('/cases', addCaseItem)
.get('/cases/:id', getCaseItem)
.put('/cases/:id', updateCaseItem)
.delete('/cases/:id', deleteCaseItem);

export default router;

Schritt 7: Applikation

Als letztes refaktorieren wir unsere am Anfang geschriebene app.ts , denn diese war nur für Testzwecke geschrieben worden. Den Router möchten wir aus routes.ts importieren. Den / Pfad löschen wir dagegen.

import { Application } from 'https://deno.land/x/oak/mod.ts';
import router from './routes.ts';

const app = new Application();
const port: number = 8080;

app.use(router.routes());
app.use(router.allowedMethods());

app.addEventListener('listen', ({ secure, hostname, port }) => {
  const protocol = secure ? 'https://' : 'http://';
  const url = `${protocol}${hostname ?? 'localhost'}:${port}`;
  console.info(
    `Listening on ${url}`,
  );
});

await app.listen({ port });

Schritt 8: Test

Die Applikation ist damit fertig. Um unsere Applikation zu starten, geben wir diesen Befehl im Terminal ein:

deno run — allow-net app.ts

Wenn wir folgende Ausgabe im Terminal sehen, dann sollte die Applikation bereit sein:

In Postman testen wir zuerst, ob wir alle Anträge aus unserer Datenbank bekommen können. Wenn wir http://localhost:8080/cases als GET Methode eingeben, sollten wir den derzeitigen Stand unserer Datenbank erhalten:

Als nächstes wollen wir nachfolgenden Antrag hinzufügen: 

{
   'id': '4',
   'customer': 'test',
   'product': 'Huas am See',
   'notes': 'Mit Deno erstellt'
}

Dazu schreiben wir das Antrag-Objekt in den Bodyteil und ändern die http Request Methode von GET in POST .

Zur Überprüfung fragen wir wieder den aktuellen Stand unserer Datenbank ab und sehen, dass die Datenbank unser eben hinzugefügtes Element enthält:

Wir hätten es auch überprüfen können, in dem wir http://localhost:8080/cases/4 aufrufen:

Nun wollen wir einen bestehenden Antrag ändern. Wir fügen bspw. eine neue Notiz hinzu:

{
   'notes': 'Neue Notiz'
}

 Dazu ändern wir die Request Methode in PUT :

Eine kurze Überprüfung zeigt, dass sich der Antrag mit der id 4 aktualisiert hat:

Zum Schluss löschen wir den Antrag mit DELETE :

Eine Überprüfung mit GET http://localhost:8080/cases zeigt, dass der Antrag mit der id 4 nicht mehr existiert:

Den gesamten Code könnt ihr hier herunterladen.

Fazit

Mit wenig Aufwand haben wir eine kleine REST Application mit CRUD Funktionen geschrieben. Besonders die eingebauten Features, wie z.B. TypeScript und der Formatierer, machen das Entwickeln angenehm.

Jedoch kommt Deno nicht ohne Kinderkrankheiten aus. Beim Hochladen des Moduls auf https://deno.land/x , ein Modul Repository, das von Deno verwaltet wird, bekam ich diesen Fehler:

No uploaded versions
This module name has been reserved for a repository, but no versions have been uploaded yet. Modules that do not upload a version within 30 days of registration will be removed.

Doch der Fehler lag nicht bei mir, sondern bei Deno. Denn nach ca. 2 Tagen konnte ich mein Modul finden. (am Anfang habe ich das natürlich nicht gewusst und dadurch noch ein weiteres Interhyp Modul hochgeladen. Jetzt gibt es auf Deno zwei Interhyp Module. Sorry Interhyp…)

Eigentlich ist der Formatier nichts Besonderes. Bei Node gibt es dafür “js-beautify”. Aber wenn ihr euch den Artikel noch mal durchlest, fällt euch bestimmt auf, dass ich den Formatierer vorher nie heruntergeladen oder konfiguriert habe. Das liegt daran, dass der Formatierer in Deno bereits eingebaut ist. Die Idee, dass Hilfsmodule standardmäßig eingebaut sind, finde ich gut. Jedoch kann man ihn nicht anpassen. Der Formatierer benutzt Leerzeichen statt Tabs und es gibt keine Möglichkeit dies zu ändern, ohne den Sourcecode zu ändern.

Bei Node ist es so, dass man sich die Tools zusammensuchen muss. Bei Deno fühlt es sich an, als wäre alles aus einem Guss — die Tools, die man sowieso braucht, sind schon mitgeliefert. Vergleicht man das mit Deno, fühlt es sich so an, als würde man ein DIY Kit zusammenstellen, wie damals die ersten Computer. Bei Deno merkt man, dass man auf eine langjährige Erfahrung mit Node zurückgegriffen hat.

Deno fühlt sich noch nicht hundertprozentig fertig an. Braucht es aber auch nicht mit ca. 2 Jahren. Um Produktionscode zu produzieren wäre ich noch vorsichtig. Aber um erste Prototypen zu erstellen eignet sich Deno nicht schlecht. Mit Deno kann man einfach mal machen — eines der Prinzipien bei der Interhyp. 


Schreibt doch bitte eure Erfahrung mit Deno. Mich würde interessieren, wie ihr Deno seht und welche Unterschiede ihr bisher zu Node bemerkt habt. Was gefällt euch am meisten an Deno?

Schreibe einen Kommentar