Zum Inhalt

Workshop: REST-Services — vom ersten Klick bis zum Datensatz

Ein REST-Service ist eine Adresse, die ein anderes Programm aufrufen kann. Es schickt eine Anfrage hin, bekommt eine Antwort zurück — meist als JSON. In KCForms schreibst du so einen Dienst in JavaScript, direkt im Browser, ohne etwas zu installieren oder auszurollen.

Dieser Workshop führt dich in sieben Teilen dahin. Teil 1 bis 3 reichen für den Anfang; ab Teil 4 wird es für den Alltag interessant.

Was du brauchst

Ein KCForms mit Administrator-Rechten — mehr nicht. Du brauchst kein Postman, kein curl und keinen zweiten Rechner: Die eingebaute Swagger-UI ruft deine Dienste direkt aus dem Browser auf.

REST-Services laufen mit vollen Server-Rechten

Ein Dienst sieht die Datenbank so, wie der Server sie sieht — nicht so, wie der angemeldete Benutzer sie sieht. Deshalb dürfen nur Administratoren Dienste anlegen, und deshalb ist Teil 6 kein optionales Kapitel.


Teil 1 — Erst einmal zuschauen: die Swagger-UI

Bevor du etwas baust, sieh dir an, was schon da ist. KCForms bringt eine Swagger-UI mit: eine Seite, auf der jeder Dienst mit allen seinen Adressen steht — und auf der du sie anklicken und ausprobieren kannst.

1.1 Wo sie liegt

Die Adresse ist immer dieselbe: /api/rest/docs hinter deiner KCForms-Adresse. Lokal also http://localhost:5173/api/rest/docs.

Der Kopf der Swagger-UI: Titel, openapi.json, Server-Auswahl und der Authorize-Knopf

Drei Dinge siehst du hier:

Element Wofür
openapi.json Die Beschreibung aller Dienste als Datei — die gibst du jemandem, der dein System anbinden will
Servers Die Basis-Adresse, an die alle Aufrufe gehen: /api/rest/service
Authorize Hier hinterlegst du Zugangsdaten, bevor du einen geschützten Dienst aufrufst (Teil 6)

1.2 Was in der Liste steht

Darunter stehen die Dienste, nach Gruppen sortiert. Jede Zeile ist eine Adresse, die man aufrufen kann:

Die Operationen-Liste: Gruppen mit je mehreren GET-Adressen

Links steht die Methode (GET holt etwas, POST legt etwas an), daneben der Pfad, dahinter eine kurze Beschreibung. Die Gruppen und Texte kommen nicht von Swagger — die hat der Autor des Dienstes selbst hineingeschrieben. Wie, steht in Teil 7.

1.3 Einen Aufruf ausprobieren

Klick eine Zeile an. Sie klappt auf und zeigt die Parameter sowie einen Execute-Knopf:

Eine aufgeklappte Operation mit Parameters und dem Execute-Knopf

Ein Klick auf Execute schickt die Anfrage wirklich ab — gegen dein laufendes System, mit deiner Anmeldung. Darunter erscheint die Antwort:

Die Antwort: curl-Befehl, Request URL und Server response mit Statuscode 418

Lies das von unten nach oben:

  • Server response — der Statuscode und der Antwort-Inhalt. 200 heißt „hat geklappt", 404 „gibt es nicht", 418 ist hier ein Beispiel für einen selbst gewählten Code.
  • Request URL — die Adresse, die tatsächlich aufgerufen wurde.
  • Curl — derselbe Aufruf als Kommandozeilen-Befehl. Den kannst du kopieren und jemandem schicken, der kein KCForms vor sich hat.

Der curl-Kasten ist das nützlichste Feld der ganzen Seite

Wenn ein Fremdsystem deinen Dienst nicht erreicht, schick dem Kollegen den curl-Befehl aus diesem Kasten. Darin steht alles: Adresse, Methode, Header. Damit lässt sich von außen prüfen, ob es am Dienst liegt oder am Anrufer.


Teil 2 — Dein erster eigener Service

2.1 Wo Dienste entstehen

System-Administration → Schnittstellen & Verteilung → REST-Services

Die Verwaltung der REST-Services mit der Liste und dem Knopf „+ Neu"

Links die vorhandenen Dienste, rechts die Felder für den gewählten: Name (eindeutig, nur zur Verwaltung), Authentifizierung und darunter der Modul-Quelltext. Ein Klick auf + Neu legt einen leeren an.

2.2 Der kleinste Dienst, der funktioniert

Trag als Namen workshop ein und füge diesen Quelltext ein:

module.exports = {
  path: "/workshop",
  operations: [
    {
      id: "hallo",
      path: "/hallo",
      method: "GET",
      handler: function () {
        return { gruss: "Hallo Welt" };
      }
    }
  ]
};

Speichern — fertig. Kein Neustart, kein Deployment.

2.3 Ausprobieren

Lade /api/rest/docs neu. Dein Dienst steht in der Liste. Aufklappen, Execute, und die Antwort lautet:

{ "gruss": "Hallo Welt" }

2.4 Die drei Teile, aus denen jeder Dienst besteht

flowchart LR
    A["Anfrage<br/>GET /api/rest/service/workshop/hallo"] --> B["path<br/>/workshop"]
    B --> C["operation.path<br/>/hallo"]
    C --> D["handler()<br/>dein JavaScript"]
    D --> E["Antwort<br/>JSON"]
  • path — der gemeinsame Anfang aller Adressen dieses Dienstes.
  • operations — die Liste der einzelnen Adressen. Jede hat eine id (eindeutig innerhalb des Dienstes), einen path, eine method und einen handler.
  • handler — die Funktion, die läuft. Was sie zurückgibt, wird zur Antwort.

Die vollständige Adresse ist also immer: /api/rest/service + path + operation.path.

Warum module.exports?

Das ist die Schreibweise, mit der ein JavaScript-Modul sagt: „Das hier ist mein Ergebnis." KCForms liest genau dieses Objekt aus. Alles, was du oberhalb davon schreibst — Hilfsfunktionen, Konstanten — darfst du im Handler benutzen.


Teil 3 — Eingaben entgegennehmen

Ein Dienst, der immer dasselbe antwortet, ist selten nützlich. Es gibt drei Wege, ihm etwas mitzugeben.

3.1 Query-Parameter: ?name=Anna

Alles hinter dem Fragezeichen landet in query:

module.exports = {
  path: "/workshop",
  operations: [
    {
      id: "gruss",
      path: "/gruss",
      method: "GET",
      parameters: [
        { name: "name", in: "query", schema: { type: "string" } }
      ],
      handler: function () {
        return { gruss: "Hallo, " + (query.name || "Unbekannt") + "!" };
      }
    }
  ]
};

Zwei Dinge sind hier neu:

  • query.name ist der Wert aus der Adresse. Fehlt er, ist er undefined — deshalb das || "Unbekannt".
  • parameters meldet den Parameter bei Swagger an. Ohne diesen Eintrag funktioniert der Dienst genauso, aber die Swagger-UI zeigt kein Eingabefeld dafür. Für dich selbst Pflicht, sobald jemand anderes den Dienst benutzen soll.

3.2 Pfad-Parameter: /kunde/4711

Geschweifte Klammern im Pfad werden zu Platzhaltern, die in pathParameters landen:

module.exports = {
  path: "/workshop",
  operations: [
    {
      id: "kunde",
      path: "/kunde/{nr}",
      method: "GET",
      pathParameters: [ { name: "nr", schema: { type: "string" } } ],
      handler: function () {
        return { nummer: pathParameters.nr, gefunden: false };
      }
    }
  ]
};

Wann Pfad, wann Query?

Faustregel: Was das Ding identifiziert, gehört in den Pfad (/kunde/4711). Was die Abfrage einschränkt oder sortiert, gehört in die Query (?aktiv=true&seite=2).

3.3 Der Body: Daten hineinschicken

Bei POST und PUT steckt der Inhalt im Body. body.asString() gibt ihn als Text, JSON.parse macht ein Objekt daraus:

module.exports = {
  path: "/workshop",
  operations: [
    {
      id: "echo",
      path: "/echo",
      method: "POST",
      handler: function () {
        var daten = body.isEmpty() ? {} : JSON.parse(body.asString());
        return { empfangen: daten, anzahlFelder: Object.keys(daten).length };
      }
    }
  ]
};

body.isEmpty() zuerst zu fragen lohnt sich: JSON.parse("") wirft einen Fehler, und der käme beim Anrufer als 500 an.

Was im Handler sonst noch bereitsteht:

Name Was drinsteht
query Query-Parameter. Einmal → Text, mehrfach → Liste
pathParameters Die Platzhalter aus dem Pfad
body asString(), asBytes(), isEmpty()
headers Request-Header, Namen klein geschrieben. authorization wird nicht durchgereicht

Dazu die gesamte übrige Skript-API: db, forms, mail, fs, log, kc — ein REST-Handler kann dasselbe wie ein Formular-Hook.


Teil 4 — Antworten und Fehler

4.1 Der einfache Weg

Gibst du ein Objekt zurück, wird daraus JSON mit Status 200. Das reicht für die meisten Fälle:

return { ok: true, anzahl: 3 };

4.2 Status selbst bestimmen

Für alles andere gibt es Response:

module.exports = {
  path: "/workshop",
  operations: [
    {
      id: "status",
      path: "/status",
      method: "GET",
      handler: function () {
        return Response.created({ id: 42, hinweis: "angelegt" });   // 201
      }
    }
  ]
};
Helfer Status Wofür
Response.ok(inhalt) 200 hat geklappt
Response.created(inhalt) 201 etwas Neues ist entstanden
Response.noContent() 204 hat geklappt, es gibt nichts zurückzugeben
Response.badRequest(inhalt) 400 die Anfrage war falsch
Response.notFound(inhalt) 404 gibt es nicht
Response.status(code, inhalt) frei alles andere

Zusätzlich lässt sich der Inhaltstyp setzen: Response.ok("a;b").type("text/csv; charset=UTF-8").

4.3 Abbrechen mit throw

throw beendet den Handler sofort. Wirfst du einen Text, wird daraus eine 500. Wirfst du einen Response, gilt dessen Status — das ist der saubere Weg für erwartbare Fehler:

module.exports = {
  path: "/workshop",
  operations: [
    {
      id: "pruefen",
      path: "/pruefen",
      method: "GET",
      handler: function () {
        if (!query.nr) {
          throw Response.badRequest({ fehler: "nr fehlt" });     // 400
        }
        return { nr: query.nr };
      }
    }
  ]
};

Ein nackter throw "text" ergibt immer 500

500 heißt für den Anrufer „der Server hat einen Fehler". Fehlt nur ein Parameter, ist das die falsche Auskunft — dann ist 400 richtig. Nimm throw Response.badRequest(…) bzw. Response.notFound(…), sobald der Fehler vorhersehbar ist.


Teil 5 — Formulardatensätze per REST

Jetzt wird es praktisch. Über das forms-Objekt liest und schreibt dein Dienst echte Formulardatensätze — mit Detail-Tabellen, mit Prüfungen, mit Änderungsprotokoll.

5.1 Lesen

module.exports = {
  path: "/workshop",
  operations: [
    {
      id: "bestellungLesen",
      path: "/bestellung/{id}",
      method: "GET",
      pathParameters: [ { name: "id", schema: { type: "integer" } } ],
      handler: function () {
        var satz = forms.get("bestellung", Number(pathParameters.id));
        if (!satz) {
          throw Response.notFound({ fehler: "Keine Bestellung " + pathParameters.id });
        }
        return satz;            // enthält auch die Detail-Zeilen
      }
    }
  ]
};

5.2 Anlegen

forms.create liefert die neue Id zurück. Detail-Tabellen übergibst du als Array von Objekten — genau so, wie das Feld im Formular heißt:

module.exports = {
  path: "/workshop",
  operations: [
    {
      id: "bestellungAnlegen",
      path: "/bestellung",
      method: "POST",
      handler: function () {
        var daten = JSON.parse(body.asString());

        var id = forms.create("bestellung", {
          name:       daten.kunde,
          gesamt:     daten.betrag,
          positionen: daten.positionen        // kc:childTable-Feld
        });

        return Response.created({ id: id });
      }
    }
  ]
};

5.3 Erst prüfen, dann anlegen

forms.validate prüft gegen dieselben Regeln wie das Formular — Pflichtfelder, Längen, Validatoren — ohne etwas zu schreiben. Es liefert eine Liste; leer heißt gültig:

handler: function () {
  var daten  = JSON.parse(body.asString());
  var fehler = forms.validate("bestellung", daten);

  if (fehler.length) {
    return Response.status(422, { fehler: fehler });   // [{field, message}, …]
  }
  return Response.created({ id: forms.create("bestellung", daten) });
}

422 ist der freundlichere Fehler

400 heißt „ich verstehe die Anfrage nicht". 422 heißt „ich verstehe sie, aber der Inhalt ist fachlich falsch" — und genau das ist ein fehlendes Pflichtfeld. Der Anrufer bekommt mit fehler eine Liste, die er seinem Benutzer anzeigen kann.

5.4 Ändern und Löschen

forms.update("bestellung", 42, { gesamt: "120,00" });   // nur dieses Feld
forms.delete("bestellung", 42);                          // samt Anlagen

forms.update ist ein Teil-Update: Was du nicht übergibst, bleibt stehen.

Ein REST-Dienst umgeht die Datensatz-Rechte

forms.get und forms.list liefern alle Datensätze, nicht nur die des angemeldeten Benutzers — der Dienst läuft ja mit Server-Rechten. Wenn ein Anrufer nur seine eigenen sehen darf, musst du das selbst einschränken (opts mit scope, oder in der Abfrage filtern).


Teil 6 — Absichern

Jeder Dienst hat eine eigene Einstellung Authentifizierung. Drei Werte stehen zur Wahl.

6.1 Bearer-Token (der Normalfall)

Du vergibst ein Geheimnis, der Anrufer schickt es im Header mit:

Authorization: Bearer mein-geheimes-token

In der Swagger-UI klickst du dafür auf Authorize und trägst es einmal ein — danach gilt es für alle Aufrufe dieser Sitzung:

Der Authorize-Dialog mit bearerAuth und basicAuth

6.2 Basic

Benutzername und Passwort, wie bei einer Anmeldemaske. Das sind keine KCForms-Benutzerkonten, sondern ein Paar, das du am Dienst hinterlegst.

6.3 „Keine"

Jeder, der die Adresse kennt, darf den Dienst aufrufen. Das ist für einen reinen Lese-Dienst im internen Netz vertretbar — für alles, was schreibt, nicht.

Ein Bearer-Service ohne eingetragenen Token ist offen

Stellst du auf Bearer um und lässt das Token-Feld leer, prüft der Dienst nichts. Trag das Token vor dem Speichern ein, und prüfe danach mit einem Aufruf ohne Authorize, dass wirklich 401 kommt.


Teil 7 — Schön dokumentieren

Alles, was in der Swagger-UI an Texten und Gruppen steht, kommt aus deinem Modul. Mit documentation und ein paar Feldern je Operation wird aus einer nackten Liste eine lesbare Schnittstellenbeschreibung:

module.exports = {
  path: "/workshop",
  documentation: {
    info: { title: "Workshop-Dienst", version: "1.0" },
    tags: [ { name: "demo", description: "Beispiele aus dem Workshop" } ]
  },
  operations: [
    {
      id: "doku",
      path: "/doku",
      method: "GET",
      tags: [ "demo" ],
      summary: "Zeigt, wie Beschreibungen in Swagger ankommen",
      handler: function () {
        return { ok: true };
      }
    }
  ]
};
Feld Wirkung in der Swagger-UI
documentation.info.title Überschrift der Seite
tags am Service Die Gruppen samt Beschreibung
tags an der Operation In welche Gruppe die Zeile einsortiert wird
summary Der Text hinter dem Pfad
parameters / pathParameters Die Eingabefelder bei „Try it out"

Die Beschreibung ist kein Zierrat

Wer deinen Dienst anbindet, liest nur die Swagger-Seite — nicht deinen Quelltext. Eine Operation ohne summary zwingt ihn, dich zu fragen. Zwei Minuten Tippen sparen eine Mail.


Wenn etwas nicht geht

Symptom Ursache
404 auf deine Adresse Pfad falsch zusammengesetzt. Er lautet /api/rest/service + path + operation.path
405 Den Pfad gibt es, aber nicht für diese Methode (GET statt POST?)
401 Der Dienst ist geschützt — in der Swagger-UI Authorize benutzen
500 mit {"error": "…"} Dein Handler hat geworfen. Die Meldung steht im Text
Der Dienst taucht in Swagger nicht auf Nicht gespeichert, nicht freigeschaltet, oder zwei Dienste benutzen denselben path
Änderung wirkt nicht Swagger-Seite neu laden — sie hält die Beschreibung im Browser-Cache

Kurz-Referenz

module.exports = {
  path: "/basis",                       // Anfang aller Adressen
  documentation: { info: {…}, tags: […] },
  operations: [
    {
      id:   "eindeutig",                // je Dienst einmalig
      path: "/unter/{platzhalter}",
      method: "GET",                    // GET POST PUT PATCH DELETE
      tags: [ "gruppe" ],
      summary: "Was die Operation tut",
      parameters:     [ { name: "q",  in: "query", schema: { type: "string" } } ],
      pathParameters: [ { name: "platzhalter", schema: { type: "string" } } ],
      handler: function () { return { ok: true }; }
    }
  ]
};

Im Handler verfügbar: query, pathParameters, body, headers, Response — plus die komplette Skript-API (db, forms, mail, fs, log, kc, …).


Weiterlesen