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.

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:

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:

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

Lies das von unten nach oben:
- Server response — der Statuscode und der Antwort-Inhalt.
200heißt „hat geklappt",404„gibt es nicht",418ist 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

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:
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 eineid(eindeutig innerhalb des Dienstes), einenpath, einemethodund einenhandler.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.nameist der Wert aus der Adresse. Fehlt er, ist erundefined— deshalb das|| "Unbekannt".parametersmeldet 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:
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:
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:

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¶
rest— REST-Services programmieren — die vollständige Referenz mit allen Feldern, CRUD-Beispiel und Health-Check.- REST-Services (Beispielsammlung) — kurze, lauffähige Dienste zum Abschauen.
forms— Datensätze im Skript — alles, was Teil 5 nur streift.- Benutzer und Rechte — warum ein Dienst mit Server-Rechten läuft.