Wie Adonis die zukunftsweisende Implementation von Node.js sein könnte

06.07.2021 Ricky Elfner
Mobile Tech Adonis.js Node.js Open source JavaScript Framework Hands-on Tutorial How-to

Adonis JS ist ein weiteres Framework mit dem wir uns diese Woche beschäftigen. Dieses befindet sich mittlerweile in der Version 5.0.

Grundlegendes

AdonisJS wurde 2015 als Open Source-Projekt gestartet und sollte als ein alternatives Framework für das Node.js-Ökosystem dienen. Dabei liegt der Vorteil bei Adonis, dass er verschiedene grundlegende Funktionen von Haus aus mit sich bringt. Deshalb müssen Sie nicht auf verschiedene Libraries zurückgreifen, um diese Funktionen auch zu nutzen.

Dazu gehört beispielsweise ein SQL-ORM, inklusive Query Builder, Migrations und Models. Ebenfalls dabei sind unterschiedliche HTTP Routing-Features, Form-Validierung, und Health Checks-Modules, welche mit Kubernetes verwendet werden können.

Projekt Struktur

Mit der Einführung der Version 5.0 werden neue Projekt mittlerweile direkt über npm init oder yarn create erstellt. Ebenfalls neu ist die Verwendung von TypeScript.

Beim Erstellen eines neuen Projekts haben Sie die Möglichkeit, zwischen den beiden Projektstrukturen API Server oder Web Applikation zu entscheiden. Entscheiden Sie sich für eine vollwertige serverseitige Web Applikation, werden zu den Core Funktionen zusätzlich eine AdonisJS Template Engine, das Session Module, sowie das Modul für Web Security & CSRF Protection mit installiert.

Entscheiden sie sich jedoch für den API Server, werden hier nur die notwendigsten Dinge installiert, die es für einen JSON API Server benötigt.

Authentifizierung

Über das Adonis Framework wird Ihnen direkt ein Authentifizierungssystem bereitgestellt. Dies ermöglicht Ihnen, ohne grossen Aufwand in Kombination mit einer Datenbank ihre Webapplikation zu schützen. Dabei wird sich hier auch nicht auf das Styling fokussiert, sondern Adonis möchte diese Funktion auch anderen System über die verschiedenen APIs zur Verfügung stellen.

Datenbanken

AdonisJS ist eines der wenigen Frameworks, welches direkten Support für SQL Datenbanken bereitstellt. Dabei werden über den Datenbank Layer Lucid verschiedenste Tools zur Verfügung gestellt, um schnell und einfach datenbasierte Applikationen zu erstellen.

Query Builder

Mithilfe der API lassen sich simple Abfragen und aber auch komplexe Join-Abfragen schnell und leicht erstellen.

const user = await Database
  .from('users')
  .select('*')
  .where('username', 'virk')
  .innerJoin('profiles', 'users.id', 'profiles.user_id')
  .first()

Data Models

Data Models stellen jeweils eine Tabelle einer Datenbank dar, dies vereinfacht Ihnen die Verwendung von Daten daraus. Haben Sie beispielsweise eine Tabelle User, so erstellen Sie ein Model mit demselben Namen. Dies ermöglicht Ihnen, die Tabelle direkt in Ihrem Code zu verwenden, wie Sie im darunter liegenden Beispiel sehen können. Dies macht Ihre Applikation auch um einiges übersichtlicher.

const user = await User.query().where('username', 'virk').first()

Schema Migrationen

Grundlegend sind Schema Migrationen da, damit Sie Datenbank Tabellen erstellen und bearbeiten können. Der grosse Vorteile, dies über den Programmcode zu machen, ist unter anderem, dass Sie die Änderungen auch an Deployment Worklflows binden können und sich so manuelle Anpassungen sparen können.

Seeds & Factories

Während der Entwicklung Ihrer Applikation haben Sie meist das Problem, dass Sie Beispiel Daten benötigen. Dies kann je nachdem, wie Sie dies handhaben, umständlich sein. Durch Seeds & Factories können Sie diesen Vorgang vereinfachen. Mit einem Seeder wird die Datenbank mit den Daten, die Sie manuell festgelegt haben, gefüllt oder mit Daten, die Sie über eine Factorie automatisch generiert haben.

Views & Templates

In diesem Framework wird Edge als Template Engine verwendet. Dabei unterstützt sie, wie die meisten Engines Dinge wie: Conditionals, Loops, Komponenten, Runtime-Debugging und Stacktraces.

Vorbereitung

Um Adonis JS in der Version 5.0 zu verwenden benötigt es:

  • Node.js >= 14.5.0

  • npm >= 6.0.0

PostgresSQL DB

Um dieses Tutorial durchführen zu können benötigen Sie zunächst einmal eine PostgreSQL DB. Sollten Sie Postgres noch nicht installiert haben, können Sie dies beispielsweise über brew machen, wenn Sie einen Mac nutzen.

brew install postgresql

Sobald die Installation abgeschlossen ist, können Sie dies mithilfe des Befehlt postgres -V über das Terminal überprüfen

Um nun fortfahren zu können müssen Sie den PostgreSQL Service starten.

brew services start postgresql

Im Anschluss sollten Sie sich einen neuen User mit eingeschränkten rechten erstellen. Dazu verbinden Sie sich erst einmal mit der postgres DB. Danach können Sie den Create-Befehl ausführen und sich für einen User Namen und ein gewünschtes Passwort entscheiden. Diese Zugangsdaten benötigen Sie später innerhalb der Webapplikation. Wen Sie den Befehl ausgeführt haben, können Sie mit \du sich alle User anzeigen lassen und somit überprüfen ob Ihre neu erstellter User dabei ist.

psql postgres;
CREATE USER [User Name] WITH PASSWORD '[Password]' CREATEDB;

Da Sie nun einen neuen User haben können Sie sich mit diem User einloggen und die DB erstellen, welche Sie später für die DB Einträge benötigen.

psql postgres -U [User Name]
CREATE DATABASE adonistodo;

Anschlissend können Sie sich mit \l alle Datenbanken anzeigen lassen. Nun können Sie sich mit \q auch ausloggen und sich bei Ihrer neuen DB einloggen.

psql adonistodo -U [User Name]

Mit \dt können Sie sich auch alle Schemas anzeigen lassen, dies wird Ihnen später helfen um zu überprüfen, ob alles Daten korrekt angelegt wurden.

Die Applikation

Das Setup

Um ein neues Projekt zu erstellen, müssen Sie über das Terminal folgenden Befehl eingeben. Dabei bestimmt der letzte Parameter den Namen.

npm init adonis-ts-app adonisjs-todo

Sobald Sie diesen Befehl ausgeführt haben, müssen Sie zwischen den beiden Projekt Strukturen API Server und Web Applikation entscheiden. In unserem Tutorial erstellen wir dabei eine Webapplikation. Ausserdem können Sie in diesem Schritt den Projektnamen noch einmal ändern und entscheiden ob das Setup für ESLit und Prettier durchgeführt werden soll. Anschliessend startet die Installation der Dependencies.

Nun können Sie in das Verzeichnis Ihrer App wechseln und mit code . VS Code öffnen.

Die Verwendung der Datenbank

Wie Sie in der Vorbereitung bereits gesehen haben, wird in diesem Tutorial eine PostgreSQL Datenbank für die Todo-Liste genutzt. Deshalb müssen Sie nun noch Lucid über das Terminal installieren.

npm i @adonisjs/lucid@alpha

Sobald dies getan ist, muss noch das Setup für Lucid durchgeführt werden. Dabei haben Sie die Möglichkeit Ihre gewünschte DB auszuwählen (PostgreSQL). Diesen Vorgang starten Sie mit dem folgenden Befehl:

node ace invoke @adonisjs/lucid

Dadurch werden die standardmässigen Konfigurationsdateien erstellt und @adonisjs/lucid wird innerhalb des  providers Arrays registriert.
Daneben müssen Sie in diesem Schritt folgende Variablen in das env.ts File hinzufügen:

PG_HOST: Env.schema.string({ format: 'host' }),
PG_PORT: Env.schema.number(),
PG_USER: Env.schema.string(),
PG_PASSWORD: Env.schema.string.optional(),
PG_DB_NAME: Env.schema.string(),

Eine weitere Anpassungen muss in dem File .env vorgenommen werden, damit ihre Webapplikation auch auf die DB zugreifen darf, bzw. kann.

PG_HOST=localhost
PG_PORT=5432
PG_USER=ricky
PG_PASSWORD=test123P
G_DB_NAME=adonistodo

Um nun auch wirklich die Datenbank mit Node verwenden zu können ist es notwendig die passenden Driver zu installieren.

npm i pg

Migration

Als nächsten erstellen Sie das Schema für die gewünschten Tabellen für Ihre Datenbank. Da es sich bei dieser App um eine Todo-Liste handelt, wird auf jeden Fall eine Tabelle für die Todo Einträge benötigt. Das File dafür können Sie über node erstellen lassen und im Anschluss die gewünschten Attribute hinzufügen.

node ace make:migration todos

Wenn dies funktioniert hat bekommen Sie die Meldung:

❯ CREATE: database/migrations/1618753413969_todos.ts

Die Funktion up vervollständigen wir mit den Attributen content und complete:

public async up () {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.string("content");
      table.integer("complete").defaultTo(0);
      table.timestamps(true)
    })
  }

Bevor Sie nun die Migration auch wirklich durchführen können, müssen Sie das Projekt erst einmal neu gebuildet werden. Dies können Sie machen, in dem Sie den Server starten:

node ace serve --watch

Sobald der Server läuft, können Sie die Migration starten. Sollte dieser Vorgang erfolgreich durchgelaufen sein bekommen Sie auch direkt die Bestätigung dafür.

node ace migration:run
❯ migrated database/migrations/1618753413969_todos

In Ihrem Terminal für die Postgres DB können Sie sich nun mit der Datenbank adonistodo verbinden und mit \dt die Liste von Relations anzeigen lassen. Dort sehen Sie nun neu todos. Dadurch können Sie sicher sein, dass alles funktioniert hat.

Models

Nun wechseln wir wieder in das Terminal für die eigentliche Applikation und erstellen ein Model für die Todo Items. Auch hierfür gibt es direkt von node einen Befehl.

node ace make:model Todo
❯ CREATE: app/Models/Todo.ts

In diesem File bestimmen wir ebenfalls die beiden Attribute content und complete, mit den zugehörigen Datentypen.

export default class Todo extends BaseModel {
  @column({ isPrimary: true })
  public id: BigInt

  @column()
  public content: String

  @column()
  public complete: Number

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime
}

Seeder

Um nun die Datenbank auch mit Beispiel Daten zu füllen, gibt es unter AdonisJs die Möglichkeit verschiedenen Seeder zu erstellen.

node ace make:seeder Todos
❯ CREATE: database/seeders/Todo.ts

In dem neu erstellten File können Sie nun gewünschte Beispiel Daten festlegen.

export default class TodoSeeder extends BaseSeeder {
  public async run () {
    await Todo.createMany([
      {
        content:'Test Content 1',
        complete:0,
      },
      {
        content:'Test Content 2',
        complete:0,
      },
      {
        content:'Test Content 3',
        complete:0,
      }
    ])
  }
}

Um nun die DB damit zu befüllen, müssen Sie zu erst Luxon für das DateTime Format noch installieren.

npm install --save luxon

Nun können Sie die DB auch wirklich befüllen:

node ace db:seed
❯ completed database/seeders/Todo

Sobald Sie innerhalb Ihres Terminals für ihrer Datenbank wechseln, können Sie nun Ihre Daten sehen.

SELECT * FROM todos;

Die Controller

Nachdem Sie nun Daten haben und einen Server, der sich ohne Probleme starten lässt, geht es nun um die eigentlichen Funktionen, die Ihr Server, bzw. Ihre Applikation haben soll. Dafür benötigen Sie in unserem Fall nur einen Controller, welche die Businesslogin enthält. Auch diesen kann man über das Terminal automatisch erstellen lassen.

node ace make:controller TodoController
❯ CREATE: app/Controllers/Http/TodosController.ts

Innerhalb dieses Controllers benötigen Sie für den Anfang einmal die Methode index, welche einfach alle vorhandenen Todo-Items aus der Datenbank abfragt. Als Rückgabewert wird die View list aufgerufen und als Daten werden die zuvor abgefragten Todo-Items übergeben.

Und eine weitere Methode ist show, welche ein bestimmtes Todo Item aus der DB zurückgibt. Diese ruft die View todo_item auf und übergibt als Daten das gefundene Item.

public async index (ctx: HttpContextContract) {
    const todos = await Todo.all()
    return ctx.view.render('list', {todos})
}

public async show({view, params}: HttpContextContract)
{
    try {
        const todo = await Todo.find(params.id);
        return view.render('todo_item', {todo});
    } catch (error) {
        console.log(error)
    }
}

Routes

Innerhalb des Files routes.ts, legen Sie die verschiedenen URLs an. Zeitgleich können Sie entscheiden, welche Controller und Methoden innerhalb dieser Seite aufgerufen werden dürfen.

Für den Anfang wird unter der Url http://127.0.0.1:3333/todos die Funktion index verwendet. Die zweite URL http://127.0.0.1:3333/list/[todo item id] nutzt die Funktion show, und zeigt anhand der Id im Link das entsprechende Item an.

Route.resource('todos', 'TodosController').only(['index']).apiOnly();
Route.resource('list', 'TodosController').only(['show']).apiOnly();

Views

Um die Views, welche innerhalb des Controllers angegeben worden sind nun auch verwenden zu können, müssen Sie diese selbstverständlich erstellen.

node ace make:view list
❯ CREATE: resources/views/list.edge

Darin können Sie für den Anfang nun einfach mithilfe eines foreach-Loops alle vorhandenen Todo-Items anzeigen lassen.

@each (todo in todos)
    {{todo.content}} - {{todo.id}}
@endeach

In Ihrem Browser können Sie die festgelegte Adresse aufrufen, damit Sie Ihre Items sehen: http://127.0.0.1:3333/todos.

Für die zweite View wird noch das zweite Template todo_item erstellt.

node ace make:view todo_item
❯ CREATE: resources/views/todo_item.edge

Für den Anfang wollen wir dort nur den Text des Items anzeigen lassen.

{{todo.content}}

Auch können Sie über beispielsweise über die URL http://127.0.0.1:3333/list/1 das Todo-Item mit der Id “1“ aufrufen.

Hinzufügen von Items

Kommen wir nun zu der ersten grundlegenden Funktion einer Todo-Liste, das hinzufügen von neuen Einträgen. Hierfür benötigen Sie innerhalb des TodoController zunächst einmal eine neue Funktion, die diese Aufgabe ausführt.

Zuerst wird dafür ein neues Todo-Objekt erstellt. Die Id wird dabei anhand von der aktuellen Zeit festgelegt, damit diese auch eindeutig bleibt. Der Inhalt wird aus dem Input Feld mit dem Namen contentüber den Parameter request abgefragt. Der complete-Status wird standardmässig auf 0 gesetzt. Anschliessend kann das Item in der DB gespeichert werden. Sobald dies getan ist, wird man zu der Seite geleitet, bei der das einzelne Item angezeigt wird.

public async add({ request, response }: HttpContextContract) {
      const todo = new Todo();
      todo.id = BigInt(Date.now());
      todo.content = request.input("content")
      todo.complete = 0;

      await todo.save();
      response.redirect("/list/" + todo.id);
  }

Auch neue Routen müssen Sie für diese Funktionsweite festlegen. Sollte die Url mit /add aufgerufen werden, wird man auf die Startseite zurückgeleitet. Wird ein Post durchgeführt, wird hierfür die Funktion add aus dem TodosController aufgerufen.

Route.on("add").render("welcome");
Route.post("add", "TodosController.add");

Als letzten Schritt für die Funktionsweise benötigen Sie noch ein Input Feld und einen Button innerhalb des Templates list.edge

<div class="formArea">
    <form class="form" action="{{ route('TodosController.add') }}" method="post">
        <input class="itemInput" type="text" name="content" value="{{ flashMessages.get('content') || '' }}" />
        <button class="button" type="submit">Add</button>
    </form>
</div>

Nun können Sie ihre eigenen neuen Items erstellen.

Löschen von Items

Selbstverständlich wollen Sie auch noch Items löschen können. Auch hier sind wieder drei Schritte notwendig. Zuerst erstellen Sie eine Funktion innerhalb des TodosController. Anhat der ID, die übergeben wird, wird das entsprechende Item gesucht und direkt gelöscht. Sobald dies erfolgreich war, werden Sie wieder auf die Todos Seite weitergeleitet.

public async delete({response, request}: HttpContextContract)
    {
        await Todo.query().where('id', request.input('del')).delete();
       return response.redirect('/todos');
    }

Dazu kommt auch wieder derselbe Aufbau der beiden Routes, mit dem Unterschied, dass es sich um die Funktionsweise Delete handelt.

Route.on("delete").render("welcome");
Route.post("delete", "TodosController.delete");

Für das löschen benötigen Sie noch einen Button, welchen Sie klicken können, um ein Item zu löschen.

<form class="button" action="{{ route('TodosController.delete') }}" method="post">
    <button class="button" type="submit" value="{{todo.id}}" name="del">X</button>
</form>

Abhaken von Todo Einträgen

Und zu guter letzt, denselben Ablauf noch ein drittes Mal. Hier wird die Methode done erstellt, welche zuerst das Item anhand der Id sucht, welche aus dem Parameter Request genommen wird. Wenn ein passenden Todo-Item gefunden wird, wird der Status umgekehrt. Dadurch wird unterschieden ob, das Item erledigt ist oder noch ausstehend ist.

    public async done({response, request}: HttpContextContract)
    {
        const todo = await Todo.find(request.input('done'));

        if(todo){
            todo.complete = todo.complete == 0 ? 1 : 0    
            await todo.save();
        }
        
        return response.redirect('/todos');
    }

Anschliessend müssen wieder zwei neue Routen angelegt werden.

Route.on("done").render("welcome");
Route.post("done", "TodosController.done");

Und damit sich auch etwas an dem Styling ändert müssen wir Status abhängige Stylings hinzufügen. Ebenfalls müssen wir einen Button hinzufügen, der den Status verändert.

<div class="listArea">
    @each (todo in todos)
        <div class="itemarea">
            <div class="listItem" key={{todo.id}}>
            <p style="{{ todo.complete > 0 ? 'text-decoration: line-through' : '' }}">
                {{todo.content}} - {{ todo.complete }}
            </p>
            </div>
            <form class="doneForm" action="{{ route('TodosController.done') }}" method="post">
                <button class="button" type="submit" value="{{todo.id}}" name="done">Done</button>
            </form>
            <form class="deleteForm" action="{{ route('TodosController.delete') }}" method="post">
                <button class="button" type="submit" value="{{todo.id}}" name="del">X</button>
            </form>
        </div>
    @endeach
</div>

Nun haben Sie es geschafft eine Webapplikation mit dem Framework Adonis JS zu erstellen. Ebenfalls interessant für Sie könnten unsere weiteren ToDo-Tutorials sein, die wir bereits mit Angular, React & Vue.js erstellt haben. Wenn Sie daran interessiert sind, bietet dies Ihnen die perfekte Möglichkeit die Unterschiede zwischen den Frameworks kennen zulernen.

Ricky Elfner – Denker, Überlebenskünstler, Gadget-Sammler. Dabei ist er immer auf der Suche nach neuen Innovationen, sowie Tech News, um immer über aktuelle Themen schreiben zu können.