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 deleteCaseitem
kö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?