Photo by Priscilla Du Preez on Unsplash

Local First Entwicklung im Frontend

Denis Lutz
8 min readJun 24, 2019

--

Einleitung

Gewöhnliche Front-End-Anwendungen müssen fast immer Daten von anderen Datenlieferanten konsumieren, meistens von einem Backend-System. Oft sind jedoch die Backends schwer-gewichtig, stehen nicht sofort zur Verfügung oder können generell nicht von der lokalen Entwickler-Umgebung angesprochen werden.

An dieser Stelle hat sich in meiner Entwicklungspraxis das Konzept der „local first“, auf Deutsch, der „lokal-zuerst“-Entwicklung sehr bewährt.

Was ist “local first” ?

Unter „lokal-zuerst“, versteht man die Arbeitsweise, bei der bereits auf der lokalen Maschine alles so nachstellt wird, wie es auch auf anderen Umgebungen vorgeplant wurde. Vor allem das Backend-System wird daher immer als ein lokaler HTTP-Server aufgesetzt, welches mit genau der selben REST-Definition funktioniert.

Motivation

Wenn man sich nun fragt, wozu derartiges Vorgehen nötig ist oder der Mehrwert generell fraglich erscheint, möchte ich folgende kritische Projekt-Situationen als Indikatoren kurz benennen:

  • Du musst bereits auf deinem Rechner gegen eine lokale Backend-Umgebung integrieren, dessen Build länger als 10 Sekunden oder gar über eine Minute dauert, dabei sind eigentlich nur Änderungen im Front-End notwendig.
  • Du musst ständig auf DEV- oder andere Umgebungen ausrollen, um die Integrität mit der REST-API zu prüfen.
  • Dein Javascript-Code muss einen komplizierten Token-Refresh-Workflow implementieren, den du nur manuell, durch HTTP Request-Manipulation im Debugger und nur auf externer Umgebung nachstellen kannst.
  • Deine Backend-API befindet sich parallel in Entwicklung, es ist unklar ob diese zeitlich fertig wird.
  • Du erwartest Freischaltungen von anderen Unternehmensabteilungen, bis die API verfügbar wird.
  • Du musst lokal ein Backend lange aufsetzen, diesen täglich updaten oder sogar deine Backend-Kollegen um Hilfe fragen, damit dein Frontend überhaupt dagegen entwickelt werden kann.
  • Obwohl das Storybook Framework eingesetzt wird und Komponenten teilweise gerendert werden, gibt es keinen Weg um die Gesamtanwendung schnell auszuführen

Wenn du beschriebe Szenarien in deinem Projekt ähnlich vorfindest, dann ist die lokal-zuerst Entwicklung höchst empfehlenswert.

Bezug zur Kontinuierlicher Integration (CI)

Mit lokal-zuerst erfüllt man eigentlich zwei der Grundpfeiler der Continious Integration, die bei den klassischen Quellen, wie z.B. Martin Fowler, definiert wurden.

  1. Teste in einer Kopie von deiner Produktionsumgebung: dies erreicht man , durch eine Nachstellung der externen REST-API auf der lokalen Umgebung.
  2. Halte den Build kurzläufig: mit einer schnell integrierbaren Kopie der externen Umgebung, reduziert sich dein Build auf reine Javascript-Build-Updates.

Umsetzung

Zur Umsetzung ist vor allem zu sagen, dass es einfacher geht als es klingt, denn gute Werkzeuge gibt es bereits, wahrscheinlich hast du schon einige auch selbst gesehen.

Zu Grunde liegt wie immer die REST-API, diese muss lokal nachgestellt werden.

Die Schnittstelle unterteilt sich in zwei grosse Bereiche:

  • die Endpunk-Pfade mit den HTTP Verben
  • das Schema der Domänen-Objekte (Attribute, Ihre Typen und Beziehungen zu anderen Datensätzen ), mittels JSON-Standard definiert

Glücklicherweise folgt eine gute REST-API indirekt dem Domänen-Modell selbst, so dass eigentlich die Endpunkte direkt vom Schema abgeleitet werden können und man durch JSON allein in vielen Fällen die Schnittstelle definieren kann. Somit dient JSON fast immer als genügende Kontraktgrundlage der Schnittstellen.

Grundlegendes zu JSON

Generell hat es sich bewährt JSON-Strukturen von vorne rein dynamisch aufzubauen.

In Javascript bedeutet es, dass man durchgehend Objektbäume definiert, welche dann in eine JSON-Datei geschrieben werden. Direkt kopiertes JSON zu nutzen scheint am Anfang zwar sehr bequem, bringt jedoch von vorne rein viele Wiederholungen in den Beispiel-Daten, vor allem jedoch, verhindert es eine dynamische Generierung von Datensätzen in naher Zukunft. Wenn du jedoch sicher weist, dass dein JSON sich kaum ändern kann es natürlich auch direkt verwendet werden.

Eins der wichtigsten Punkte, ist die „best practice“ die generierten Daten auch in den Unit-Tests zu nutzen. Somit diktiert man bewusst das Objekt-Schema von einer Stelle. Muss ein Attribut geändert werden, so passiert es wirklich zentral und erspart potentielle Fehlerquellen. Wie empfindlich Javascript an dieser Stelle ist möchte ich in folgendem Beispiel demonstrieren.

Dein trainiertes Auge sieht es bestimmt schon:

Verschreibt man sich nur bei einem Attribut, so schleichen sich undefinierte Werte in Objekten ein. Die Lösung, ist die strikte Regel JSON Inhalte zentral auszulesen, anstatt viele Kopien einzelner Objekte oder alleinstehende Objektbäume nur in Tests anzulegen. Auch wenn es heisst ein Test sollte selbst-enthaltend sein, kann er seine Test-Daten von einer externen Quelle beziehen. Hierfür empfehle ich eine Service-Klasse zur Test-Datenverwaltung, welche dann von den Tests genutzt wird und für Datenintegrität sorgt.

Backend-Server

Nach der Datenverwaltung, möchte ich auf die eigentlichen Server-Lösungen eingehen, die unser Backend replizieren werden. Die Anforderung an dieser Stelle, ist es die Anwendung wie der Endbenutzer lokal auszuführen zu können, somit handelt es sich um einen manuellen End-User-Test, der am Ende der Test-Pyramide liegt. Wichtig ist es ausschliesslich HTTP Server zu nutzen um eben möglichst realitätsnah zu arbeiten. Die Mock-Bibliotheken wie „Nock“, halte ich an dieser Stelle für unpassend, weil Sie einen anderen Test Fokus haben und leider auch durch sich selbst sofort Komplexität erzeugen.

JSON-Server

Das Projekt JSON-Server ist der erste bewährte Kandidat eines lokalen HTTP-Servers. Dieser benötigt nur JSON-Dateien um schnell einen vollen REST-Service nachzubilden.

Installation nach offizieller Dokumentation.

npm install -g json-server

Man generiere zuvor folgende JSON Struktur, welche in der Datei mit dem Namen db.json abgelegt wird. Hierzu ist die Methode JSON.stringify() sehr passend.

{"posts": [
{ "id": 1, "title": "json-server", "author": "typicode" }
],
"comments": [
{ "id": 1, "body": "some comment", "postId": 1 }
]}

Sonst brauchen wir noch ein Start-Script, der wie folgt aussieht (gleich in package.json definieren)

json-server --watch db.json

Und schon kann die erste Response aufrufen werden:

http://localhost:3000/posts/1

Wie man sieht, folgt die erste Route selbst der JSON-Struktur, dementsprechend muss man eigentlich nichts mehr anpassen, vorausgesetzt die REST-API folgt gewöhnlichen Konventionen.

Nun stell dir vor, du kriegst eine Beispiel-Response vom Backend und kannst nach wenigen Minuten dagegen lokal entwickeln, sogar vollständige CRUD Operationen sind möglich. Dazu sagt die Dokumentation von JSON-Server:

If you make POST, PUT, PATCH or DELETE requests, changes will be automatically and safely saved to db.json using lowdb.“

Neben dem Basis-Setup und als ein Freund der 80-zu-20-Regel, würde ich noch folgende Sachen als notwendiges Wissen festlegen.

Eine Id bei jedem JSON-Objekt ist potentiell erforderlich: Dies erfordert der JSON Server selbst, damit die Entität-Modifikationen gemacht werden können. Am Anfang sieht man öfters diese Aufforderung, wenn man z.B. ein JSON-Objekt löschen möchte. Ist ein „Id“-Attribut auf der betroffenen Entität gar nicht geplant, so ist es jedem selbst überlassen diesen eventuell an dieser Stelle einzuführen. Damit bequem gearbeitet werden kann.

Port Configuration:

json-server --watch db.json –port 3004

Extra Routen:

json-server --watch db.json –port 3004 --routes routes.json

Der Inhalt von routes.json definiert dann deine Routen.

Hier definieren wir die Routen, die dein Frontend lokal absetzen kann. Jede Route wird dann auf die JSON-Server-Routen umgeleitet. Bequem sind vor allem die Platzhalter für dynamische Routenabschnitte, hier :id oder die Query-Parameter.

Middlewares

Auch eine Middleware kann angehängt werden, mit der du dann sehr viel Kontrolle über deine HTTP-Requests gewinnst. Mehr dazu findet man hier.

Mehr ist es nicht, aus meiner Erfahrung kannst du bereits mit diesem Setup diverse Oberflächen für gängige Microservices entwickeln.

Express.js

Ist am Anfang klar, dass es ein grosses Projekt wird, man viele HTTP-Abläufe lokal nachstellen möchte, volle Kontrolle über Routing und Middleware braucht, so steht einem vollständigem Express.js als Server nichts im Weg. Dieser ist etwas grösser beim Aufsetzen, danach jedoch erfüllt es alle notwendigen Anforderungen und hat vollständige Funktionalität eines REST-Servers.

Hier gelten dieselben Prinzipien wie bei dem JSON-Server. Weil Express.js bekanntes Framework bereits ist, verweise ich hier nur auf Dokumentation.

Frontend einbinden

Der Standard: Storybook

Ist unser Backend bereit, so müssen wir auch das Frontend selbst lokal ausführen. Der verbreitete Standard an dieser Stelle ist sicherlich das Storybook. Dieser ist meiner Meinung nach fast immer empfehlenswert. Sogar wenn man eine Single Page Application bauen muss, ermöglicht es Navigationspfade und Komponenten-Zustände zu definieren, ohne zu diesen immer wieder bei der Entwicklung navigieren zu müssen. Unschlagbar ist Storybook bei der Entwicklung von Komponenten-Bibliotheken, in Kombination mit dem Knobs-Plugin macht es ein Interaktives entwicklen mit sehr vielen Properties möglich.

Webpack-Dev-Server

Als das Standard Werkzeug zum Bündeln von Artifakten, bietet auch Webpack einen lokalen Server zur Entwicklung.

Simple HTTP Server

Natürlich kann ein beliebiger HTTP Server ebenfalls das Frontend lokal einbinden, mein eigener Favorit ist http-server. Dieser lässt sich sehr leicht installieren und starten.

Umgebungen definieren

Ein letzter Punkt den ich erwähnen möchte, ist die Praxis, die Laufzeit-Umgebungen von vorne rein klar zu definieren, dabei kann es bereits lokal, mehr als eine sein.

Es können dann beispielsweise folgende Umgebungen existieren.

  • TEST — diese wird ausschließlich für Unit-Tests genutzt, hierbei leiten sich bestimmte URL und Konstanten wie der eingesetzte Benutzer oder sein Auth-Schlüssel ab.
  • LOCAL-DEV — diese Umgebung geht direkt von dem Entwickler-Rechner gegen eine externe DEV-Umgebung somit variieren alle URLs und Konstanten
  • DEV, INT und PRD sind dann gewöhnliche Umgebungen die man aus anderen Projekten kennt, die sensitiven Konstanten hierfür werden dann auf der jeweiligen Umgebung separat eingebunden.

Ein gutes Werkzeug für diesen Setup ist das Projekt „Convict“. Dieser deckt die typischen Anforderungen nach den besten Methoden ab.

Überblick

Zusammenfassend entsteht in etwa die folgende Struktur bezüglich Umgebungen, JSON-Definition und API-Beziehungen. Bitte hier beachten dass es sich hierbei nur um eine abstrakte Skizze und kein valides UML handelt.

Fazit

Betrachtet man lokal-zuerst, so bietet es trotz des einfachen Prinzips einige Vorteile. Aus meiner Erfahrung war ich oft froh, den lokalen Setup einfach bei mir zu haben und betrachte es als die „Geheimwaffe“ eines vorausschauenden Entwicklers.

Speziell bei einem Team mit vielen Junior-Entwicklern oder auch grossen Teams, ist die lokal-zuerst Entwicklung als feste Projekt-Vorgabe empfehlenswert, weil es oft schnell aus dem Bild gerät, wie viel Entwicklungszeit auf Integrations-Vorgänge verbraucht wird und man ganze Projekte damit verlangsamen kann.

Mit den vorgestellten Werkzeugen, kann man auch nicht mehr sagen, dass dieses Prinzip einen grossen Aufwand erfordert, weder initial noch im weiteren Projektverlauf.

--

--