Einführung

Als Programmierer schwärmen wir oft für die technisch vollständigsten Lösungen und ziehen es vor, unsere Dienste von Grund auf selbst zu schreiben. Das ist sowohl ein Segen als auch ein Fluch: Die Kontrolle, Effizienz und Leistungsfähigkeit, alles selbst zu schreiben, geht auf Kosten der Entwicklungsgeschwindigkeit. Als Teil unseres Cloudflight Technical Lab haben wir uns entschieden, verschiedene Lösungen zu erkunden, die bei der schnelleren Lieferung von Software helfen – schnelle Entwicklung. Das ist besonders nützlich, wenn unsere Teams gebeten werden, ein funktionales MVP in sehr kurzer Zeit zu liefern.

Wenn ihr mehr über die Herausforderung erfahren möchtet, lest bitte zuerst den Einführungsartikel.

Tech-Stack

Flowchart showing Vue.js connecting to Strapi, enabling rapid development throughout the system. Strapi seamlessly links to both Postgres and RabbitMQ. RabbitMQ manages email, retry, and error queues that connect to a Mail Sender, which in turn loops back to the retry and error queues.

Strapi

Strapi ist der größte Verkaufsschlager unserer Lösung. Es ist ein leistungsstarkes, quelloffenes Node.js- und TypeScript-Content-Management-System (CMS). Im Kern ist es eine Abkürzung für die Erstellung von REST- oder GraphQL-APIs, die die Notwendigkeit ersetzt, den gesamten Backend-Code selbst zu schreiben. Der CMS-Teil, Content-Management-System, bedeutet im Wesentlichen, dass Strapi ein Admin-Dashboard bietet, das auf einer Webanwendung läuft, wo Datenbankdaten angezeigt, neue API-Endpunkte erstellt und der Server über eine intuitive GUI aufgebaut werden können.

A user interface, built for rapid development, allows you to create a new text field called ReservationDescription in a content management system. It features options for short and long text types, seamlessly integrating with Strapi. Other menu items are listed on the left sidebar.

Strapi ist das führende quelloffene Headless-CMS. Es ist 100 % JavaScript, vollständig anpassbar und entwicklerorientiert. – Strapi

Wenn wir tiefer in die innere Funktionsweise von Strapi eintauchen, haben wir herausgefunden, dass es Koa als Web-Framework und Bookshelf.js, das von Knex angetrieben wird, als Object Relational Mapping (ORM) verwendet. Auf diesen beiden Node.js-Bibliotheken wurde ein „Strapi-Framework” aufgebaut, das vom automatisch generierten Code oder programmatisch verwendet werden kann, falls das Dashboard-Generator für einen bestimmten Fall nicht gewünscht oder nicht geeignet ist. Dieses Framework bietet generische Controller, Services zur Behandlung von CRUD-Anfragen und einen generischen Entity Service zur Behandlung von Datenbankoperationen wie Suchen, Erstellen, Aktualisieren und Löschen. Das macht es sehr einfach, Strapi anzupassen, und auch für das Dashboard, den Code zu generieren.

Ein nettes Feature des generierten Codes ist die Möglichkeit, komplexe Filter- und Join-Operationen direkt in den Anfragen hinzuzufügen. Das Format der Parameter wird vom Entity Service in eine Knex-ORM-Abfrage umgewandelt, die dann in SQL übersetzt wird. Eine weitere großartige Sache ist die Out-of-the-Box-Paginierung.

Das Anpassen von Strapi ist im Allgemeinen sehr einfach. Meistens müssen Entwickler nur einige zusätzliche Funktionen innerhalb eines Objekts hinzufügen, das als Parameter an die Konstruktoren der Controller oder Services übergeben wird. Sie können für benutzerdefinierte Validierungen oder Geschäftslogik verwendet werden, und sie haben Zugriff auf die originalen Controller, die für diese Entität generiert wurden, sowie auf alle Services und den Entity Service, sodass Entwickler das Rad nicht neu erfinden müssen. Für komplexere Anwendungsfälle können benutzerdefinierte Endpunkte und Datenbankabfragen geschrieben werden.

import {factories} from '@strapi/strapi';
export default factories.createCoreController('api::reservation.reservation', 
({strapi}) => ({
    // ...
    async find(ctx) {
        const {data, meta} = await super.find(ctx);
        // validation for admin users
        if (ctx.state.user.role.name === 'Admin') {
            return {data, meta};
        }
        // custom validation for regular users
        const userId: number = ctx.state.user.id;
        if (data.some((el) => el.attributes.users_permissions_user.data.id !== 
userId)) {
            return ctx.forbidden('Data not created by this user', {});
        }
        return {data, meta};
    },
    // ...
}));

Dieser Code erstellt im Wesentlichen einen Controller für einen Reservierungs-API-Endpunkt mit spezieller Behandlung für Admin-Benutzer und reguläre Benutzer. Admins können ohne Einschränkungen auf Reservierungsdaten zugreifen, während reguläre Benutzer nur auf Daten zugreifen können, die sie selbst erstellt haben. Wenn sie versuchen, auf Daten zuzugreifen, die von anderen Benutzern erstellt wurden, erhalten sie eine „Verboten”-Antwort.

Keiner von uns hatte vorher Erfahrung mit Strapi oder ähnlichem. Daher verwendeten wir in der ersten Arbeitswoche Pair Programming. Es erwies sich als ausgezeichnete Nutzung unserer Zeit, da wir alle zusammenarbeiteten, um zu verstehen, wie man mit Strapi arbeitet. Sobald wir uns alle an die Gesamtarchitektur unserer App gewöhnt hatten, haben wir nur noch bei den wichtigen Dingen Pair Programming gemacht. Am Ende glauben wir, dass es sehr einfach und intuitiv zu bedienen ist, da ein paar Tage Recherche ausreichten, um Strapi in vollem Umfang als selbstständigen komplexen Backend-Dienst zu nutzen.

Plugins

Die wahre Stärke von Strapi liegt in seinen Plugins, die kostenlos aus Strapis Marktplatz (eingebaut in das Admin-Dashboard) hinzugefügt werden können. Sie fügen Strapi zusätzliche Funktionalitäten hinzu und erweitern seine Möglichkeiten ohne zusätzlichen Code. Einige der nützlichsten Plugins, die wir entdeckt haben:

Documentation: Generiert ein OpenAPI-Dokument für alle Endpunkte von Strapi, das entweder im Browser geöffnet oder zur Codegenerierung verwendet werden kann. Dieses Plugin hinzufügen, um alle verfügbaren Endpunkte aufzulisten und zu sehen, wie man korrekt Anfragen an Strapi stellt.

Config Sync: Ermöglicht Programmierern, Strapi-Einstellungen zwischen Umgebungen zu teilen, wie Zugriffsrechte auf verschiedene Operationen basierend auf Rollen, entweder über die CLI oder die GUI. Dieses Plugin ist ein Muss, wenn mehrere Personen am Projekt arbeiten oder wenn Strapi deployed werden soll.

Weitere Plugins, die in unserem anfänglichen Strapi-Projekt vorinstalliert waren:

Content Type Builder: Neue Datentabellen in der Datenbank über die Strapi-GUI hinzufügen. Das ermöglicht das Erstellen neuer Entitäten und CRUDs. In Strapi können Entitäten Collection Types oder Single Types sein. Der Unterschied besteht darin, dass Single Types nur einen einzigen Wert erlauben und wie Singletons wirken, während Collection Types reguläre Tabellen sind, die mehrere Zeilen unterstützen.

Content Manager: Schnelle, codefreie Möglichkeit, die Daten in der Datenbank zu sehen, zu bearbeiten und zu löschen. Das kann dem Eigentümer der Anwendung auch ermöglichen, Inhalte ohne erneutes Deployment zu verwalten.

Email: Konfiguriert die Anwendung zum Versenden von E-Mails. Dieses Plugin hilft beim Formatieren von E-Mails und deren Weiterleitung an Drittanbieter.

Media Library: Bilder laden und in der API verwenden – mehrere Bildtypen können einfach gespeichert und auf der Website verwendet werden.

Roles & Permissions: JWT-basiertes API-Sicherheits- und Benutzerverwaltungssystem. Fügt den User-Datentyp in der Anwendung hinzu, der für Authentifizierungszwecke verwendet werden kann. Es unterstützt mehrere Anbieter und mehrere Sicherheitsrollen.

Internationalization: Fügt die Möglichkeit hinzu, neue Lokalisierungen zu erstellen und i18n für die API einzurichten. Dieses Plugin ermöglicht dem Eigentümer der App, Inhalte ohne erneutes Deployment zu lokalisieren. Die API gibt den Inhalt für die richtige Lokalisierung basierend auf den API-Anfrageparametern zurück.

Performance

Wir haben Strapis Endpunkt zur Erstellung von Reservierungen mit Apache JMeter einem Lasttest unterzogen, um ein zuverlässiges, reproduzierbares Ergebnis zu erhalten. Wir verwendeten eine variable Anzahl von Benutzern und erhöhten diese im Laufe der Zeit. Bisher haben wir keine Leistungsprobleme festgestellt.

Mail-Sender

Diagram showing Strapi sending emails to RabbitMQ via a custom provider to enable rapid development; RabbitMQ routes messages to Mail Sender, with retry and error queues handling failed messages and retries.

Strapi hat ein integriertes E-Mail-Plugin, aber es sendet E-Mails nicht automatisch, sondern formatiert sie nur und leitet sie an bestimmte Anbieter weiter. Es gibt einige integrierte Anbieter (Node-Module), aber sie sind entweder Drittanbieter-Plattformen oder sehr einfach gehalten (wie nodemailer). Wenn Fehler selbst behandelt und nach einiger Zeit erneut versucht werden soll, oder wenn kein externer Dienst die E-Mails verarbeiten soll, muss ein eigener Anbieter erstellt werden. Anbieter sind Node-Module, die die Funktionalität eines Strapi-Plugins erweitern.

Da wir uns entschieden haben, den E-Mail-Versand mit RabbitMQ und einem Microservice zu implementieren, pusht unser Anbieter die formatierte E-Mail, die er vom Strapi-E-Mail-Plugin erhält, an den spezifischen RabbitMQ-Exchange. Dieser Exchange sendet sie dann an eine E-Mail-Queue, wo sie von unserem E-Mail-Verarbeitungs-Microservice gelesen wird.

Um E-Mails zuverlässig zu senden, haben wir RabbitMQ so eingerichtet, dass wir eine E-Mail-Queue, eine Retry-Queue und eine Error-Queue haben. In der E-Mail-Queue befinden sich die E-Mails, die von unserem Microservice verarbeitet werden müssen. Wenn beim Versuch, die E-Mail zu senden, ein Fehler auftritt, wird die E-Mail in die Retry-Queue gepostet, wenn die Anzahl der Versuche zum Senden der spezifischen Nachricht kleiner als eine festgelegte Konstante ist. Andernfalls wird sie in die Error-Queue gesendet. In der Retry-Queue verbleiben E-Mails für eine bestimmte Zeit (sie haben eine Lebenszeit), dann werden sie verworfen und zurück in die E-Mail-Queue gesendet.

Dieser Ansatz garantiert, dass unser Service in den meisten Fällen in der Lage sein wird, die E-Mails zu senden, und wenn nicht, verbleiben sie in der Error-Queue.

VueJS

Alle Webseiten sind in VueJS mit Composition API implementiert. Unsere frühere Erfahrung mit Angular und React machte VueJS leicht zu erlernen. Die offizielle Dokumentation und das Tutorial waren sehr hilfreich. Um die Entwicklung zu beschleunigen, haben wir generische Komponenten für alle Ansichten unserer App implementiert: ein generisches Formular mit Vuelidate für die Validierung, eine generische Tabelle mit Bearbeiten- und Löschen-Aktionen und eine generische Objekt-Detail-Karte. Mit Pinia war es auch sehr einfach, reaktive Stores zu implementieren.

Wir entschieden uns, die kostenlose Version von CoreUI als Hauptkomponentenbibliothek zu verwenden. Die Datumsauswahl-Komponente von CoreUI ist hinter einer Bezahlschranke gesperrt, und wir hatten anfangs Schwierigkeiten, eine zuverlässige Datumsauswahl-Komponente für unsere Reservierungsansicht zu finden. Wir einigten uns auf die Verwendung der Datumsauswahl von PrimeVue.

OpenAPI-Generator

Die meisten Webentwickler haben die OpenAPI-Spezifikation bereits gesehen, da sie das ist, was Swagger-Tools verwenden. Natürlich haben wir uns entschieden, diese Spezifikation für Strapi zu erstellen. Nach dem Hinzufügen des Documentation-Plugins zu unserem Projekt erhielten wir Zugang zu einer .json-Datei, die die vollständige Liste unserer Endpunkte in der OpenAPI-Spezifikation enthält. Diese .json-Datei wird jedes Mal generiert, wenn wir den Strapi-Entwicklungsserver starten. Wir können diese Datei mit Swagger öffnen, um zu sehen und besser zu verstehen, wie die von Strapi bereitgestellten APIs verwendet werden. Dieses Plugin ermöglicht die Anpassung der resultierenden Spezifikationsdatei, wie die Möglichkeit, Eigenschaften oder Endpunkte ein- oder auszuschließen.

Letztendlich haben wir die OpenAPI-Spezifikation unserer API auch verwendet, um REST-Services für das Frontend, die mit Strapi kommunizieren können, automatisch zu generieren. Das verwendete Tool generiert TypeScript-Dateien, die wir zu unserem VueJS-Projekt hinzufügen. Das war die größte Zeitersparnis in unserer Frontend-Entwicklung. Normalerweise ist das Schreiben der Frontend-REST-Services selbst eine redundante Aufgabe, die basierend auf den Spezifikationen der gewählten API automatisiert werden sollte.

Postgres

PostgreSQL ist die offizielle Datenbankempfehlung im Setup-Leitfaden, aber mehrere Datenbank-Engines werden unterstützt. Wir haben Postgres aus persönlicher Präferenz und Bequemlichkeit verwendet.

Spring Boot – wo ist es?

An orangutan with a serious expression and folded arms, contemplating rapid development. Text on image: IM DELETING SPRING. CHANGE MY MIND.

Der anfängliche Tech-Stack enthielt eine selbstverwaltete Spring-Boot-Anwendung, die als Mittelsmann zwischen dem VueJS-Frontend und dem Strapi-Backend fungierte. Wir haben diese Komplexitätsschicht in der endgültigen Lösung entfernt, weil Strapi ausreichend war, um Endpunkte zu sichern und in der Lage war, die Geschäftslogik unserer App hinzuzufügen. Daher wäre das Hinzufügen von Spring redundant gewesen und hätte nur zusätzliche Komplexität, einen weiteren potenziellen Engpass hinzugefügt und mehr Entwicklungszeit erfordert. Natürlich kann es bei größeren Anwendungen Anforderungen geben, für die Strapi nicht geeignet ist, sodass möglicherweise auch ein Spring-Backend erforderlich ist.

Vor-/Nachteilsdiskussionen

Das ist ein Überblick über die Verwendung von Strapi für unsere Backend-Dienste:

Vorteile

  • Einfache Implementierung von CRUDs für Modelle mit einfacher Logik
  • Einfach zu verwenden: Die Verwendung der GUI für Boilerplate-Code ist sehr praktisch
  • Kostenlos für jedes Projekt nutzbar, wenn es auf eigenen Servern betrieben wird
  • Kann mit benutzerdefinierten Implementierungen erweitert werden (man ist nicht auf den automatisch generierten Code beschränkt)
  • Kommt mit mehreren Authentifizierungsanbietern out-of-the-box
  • Bietet viele Plugins, sowohl offizielle als auch von der Community erstellte
  • Mehrere SQL-Datenbanktypen werden unterstützt
  • Anwendungen können sowohl vertikal als auch horizontal skaliert werden

Nachteile

  • Weniger zeiteffizient als ein selbstverwalteter Dienst
  • Mangelnde Dokumentation – Debugging kann langsam sein
  • Unerwartete Abstürze für das CMS-/Admin-Dashboard beim Bearbeiten der Schemas der Collections (kein Problem für die Produktion – das Modifizieren der Schemas ist in der Produktion deaktiviert)
  • Unvollständige Migrationsunterstützung – Migrationen laufen vor der Aktualisierung der Schemas, da Schemas von Strapi und Migrationen von Knex verwaltet werden
  • Praktisch keine Validierungsunterstützung – benutzerdefinierte Endpunkte erforderlich
  • Unterstützt MongoDB nativ nicht

Lessons Learned

Es gibt keinen Tech-Stack, der für jedes Projekt richtig ist, aber es gibt sicher das beste Werkzeug für die Aufgabe. In unserem Fall hat Strapi angesichts der funktionalen Anforderungen und der Frist gute Arbeit geleistet. Die Verwendung einer monolithischen Architektur und integrierter Strapi-Features war der schnellste Weg für uns, das MVP im gegebenen Zeitraum zu liefern. Wir haben herausgefunden, dass Strapi eine sehr leistungsstarke und vielseitige Möglichkeit ist, Backend-Dienste zu erstellen, und seine Verwendung war eine befriedigende Erfahrung. Es ist außerdem sowohl datenbank- als auch frontend-agnostisch. Wir werden es definitiv für neue Projekte in Betracht ziehen, entweder als eigenständigen, selbstständigen Server oder als Microservice, da es mehr als geeignet ist, MVPs zu liefern oder einen zuverlässigen Microservice in größeren Projekten zu erstellen. Es ist in der Lage, die Entwicklungszeit zu reduzieren, und es ist recht einfach zu erlernen. Für die beste Erfahrung empfehlen wir jedoch, die Plugins des Strapi-Projekts sofort nach der Einrichtung zu installieren, um mögliche Merge-Konflikte und andere Probleme bei der Zusammenarbeit mit Teammitgliedern zu vermeiden.

Bitte beachtet, dass es auch kleine Vorbehalte gibt, wie den, den wir kennenlernten, als wir auf Schwierigkeiten stießen, als wir versuchten, komplexe Logik und Validierungen einzuführen. Zum Beispiel hatten wir 2 Entitäten mit einer Eins-zu-Viele-Beziehung zwischen ihnen, stellten aber fest, dass Strapi kein Cascade-Delete in der Standard-API-Implementierung unterstützte (siehe Feature-Anfrage). Daher mussten wir einen Datenbank-Trigger hinzufügen, um die Enden der Eins-zu-Viele-Beziehung zu löschen.

Eine weitere Lektion, die wir gelernt haben, ist, dass Strapi skalieren kann. Wenn die Anwendung horizontal skaliert werden soll, kann ein Load-Balancer wie Nginx verwendet werden. Für zusätzliche Optimierung sollte Caching zwischen Sessions implementiert werden. Das kann durch die Verwendung des Rest-Cache-Strapi-Plugins mit einem selbstverwalteten Redis-Server erreicht werden.

Außerdem finden wir VueJS eine Technologie, die im Frontend-Framework-Ökosystem verwendenswert ist. Frühere Erfahrungen mit anderen komponentenbasierten Frameworks sind ein Plus, aber die offizielle Dokumentation und Foren sind ein großartiger Einstieg. Als ereignisgesteuerte Architektur ist es intuitiv zu erlernen und zu verwenden.

Lassen wir die Credits rollen (in alphabetischer Reihenfolge) und dieses Kapitel in unserem Software-Dev-Tagebuch abschließen: Andrei Cotor, Daniel Todașcă und Gergely-Péter Mátyás haben zu dieser Erkundung beigetragen. Denkt daran: Der Code mag kompilieren, aber die Reise ist das, was wirklich zählt. Bleibt neugierig, bleibt am Coden, und lasst uns weiterhin die Grenzen des Möglichen verschieben, eine Codezeile nach der anderen. Bis zum nächsten Mal, frohes Coden, meine Entwicklerkollegen!