REST-Services (frei programmierbar)¶
Mit REST-Services stellst du eigene, per JavaScript programmierte HTTP-Endpunkte bereit — andere Systeme (Portale, Fachanwendungen, Automatisierungen) rufen sie auf, um Daten aus KCForms zu lesen oder Aktionen auszulösen. Jeder Service ist ein kleines JS-Modul, wird in der System-Administration gepflegt, ist nach dem Speichern sofort aktiv (ohne Neustart) und bringt eine eingebettete Swagger-UI zum Ausprobieren mit.
Nur für Administratoren
Ein REST-Service läuft mit vollen Server-Rechten (voller Zugriff auf
db, fs, mail …). Die Verwaltung ist deshalb Administratoren
vorbehalten. Wie der Aufruf abgesichert ist, legst du je Service fest
(Bearer-Token, Basic oder keine).
In 30 Sekunden: der kleinste Service¶
System-Administration → Schnittstellen & Verteilung → REST-Services → + Neu, Namen vergeben, diesen Quelltext einfügen, Anlegen:
module.exports = {
path: "/hello",
operations: [
{
id: "helloWorld",
path: "",
method: "GET",
handler: function () {
return { message: "Hallo, " + (query.name || "Welt") + "!" };
}
}
]
};
Aufruf:
<server> ist die Adresse deiner KCForms-Installation (in der
Entwicklungsumgebung localhost:8099).
Das Modul¶
Ein Service exportiert genau ein Objekt:
module.exports = {
path: "/customer", // Basis-Pfad des Service
documentation: { … }, // optional: OpenAPI-Kopf, Schemas, Tags
operations: [ … ] // die einzelnen Endpunkte
};
Hilfsfunktionen und Konstanten schreibst du einfach oberhalb von
module.exports ins Modul (siehe CRUD-Beispiel). require
gibt es nicht; für gemeinsamen Code über mehrere Services hinweg siehe
Datei-Bibliotheken.
Die Aufruf-URL¶
/api/rest/service + service.path + operation.path:
service.path |
operation.path |
method |
Ergibt |
|---|---|---|---|
/customer |
"" |
GET |
GET /api/rest/service/customer |
/customer |
/{id} |
GET |
GET /api/rest/service/customer/123 |
/kcforms |
/forms/{id} |
GET |
GET /api/rest/service/kcforms/forms/abc |
Gibt es den Pfad, aber nicht für diese Methode, antwortet KCForms mit 405;
gibt es den Pfad gar nicht, mit 404.
Eine Operation¶
{
id: "getCustomer", // eindeutig innerhalb des Service
path: "/{id}", // an service.path angehängt; {…} = Parameter
method: "GET", // GET | POST | PUT | PATCH | DELETE …
summary: "Kundendaten abrufen", // Kurztitel (Swagger)
description: "Liefert einen Kunden.", // Langtext (Swagger)
tags: [ "kunden" ], // Gruppierung (Swagger)
consumes: "application/json", // erwarteter Body-Typ (Swagger)
produces: "application/json", // Antwort-Typ (auch für Text-Antworten)
pathParameters: [ { name: "id", schema: { type: "string" } } ],
queryParameters: [ { name: "aktiv", schema: { type: "boolean" } } ],
requestSchema: { "$ref": "#/components/schemas/Customer" },
responseSchema: { "$ref": "#/components/schemas/Customer" },
handler: function () { /* … deine Logik … */ }
}
Nötig sind nur id, method und handler — der Rest dient der
Swagger-Doku. Fehlt path, gilt der Basis-Pfad des Service.
Der Request im Handler¶
Im handler stehen die Request-Daten als globale Variablen bereit:
| Global | Typ | Beschreibung |
|---|---|---|
pathParameters |
Objekt | Platzhalter aus dem Pfad, z. B. pathParameters.id |
query |
Objekt | Query-Parameter; Einzelwert → String, mehrfach → Liste |
headers |
Objekt | Request-Header (Namen klein geschrieben); authorization wird nicht durchgereicht |
body |
Objekt | Request-Body: body.asString(), body.asBytes(), body.isEmpty() |
request |
Objekt | request.method, request.path, request.user |
handler: function () {
var id = pathParameters.id; // "/{id}"
var aktiv = query.aktiv === "true"; // ?aktiv=true
var daten = body.isEmpty() ? {} : JSON.parse(body.asString());
var accept = headers["accept"];
log.info("Aufruf durch " + request.user);
return { id: id, aktiv: aktiv };
}
request.user (gleichbedeutend ctx.user.name) ist der Aufrufer laut
Authentifizierung.
Die Antwort¶
Es gibt drei Wege, eine Antwort zu erzeugen.
1. Einen Wert zurückgeben → 200 OK¶
- Objekt / Array → JSON (
application/json) - String → Text (
text/plain, oder derproduces-Typ der Operation) - Bytes (z. B.
fs["/export.pdf"].readBytes()) → binär (application/octet-stream) - nichts (
undefined/null) → leere Antwort
2. Ein Response-Objekt — Status und Typ selbst bestimmen¶
3. throw für Fehler¶
handler: function () {
if (!pathParameters.id) throw "id ist Pflicht."; // 500, {"error":"id ist Pflicht."}
var c = findById(pathParameters.id);
if (!c) throw Response.notFound({ error: "Unbekannt" }); // 404
return c;
}
throw "Text"→500mit{ "error": "Text" }throw new Error("Text")→500mit{ "error": "Text" }; eigene Felder am Fehler (e.code = "K1") kommen mitthrow { … }→500mit genau diesem Objektthrow Response.xyz(…)→ der Status des Response (z. B.404)
Ein anderer Status als 500 geht nur über Response, z. B.
throw Response.badRequest({ error: "…" }).
Die Response-Helfer¶
Response steht in jedem Handler bereit — als Rückgabewert oder mit
throw. Ob es ein Fehler ist, entscheidet allein der Status (ab 400).
| Aufruf | Status |
|---|---|
Response.ok(inhalt) |
200 |
Response.created(inhalt) |
201 |
Response.accepted(inhalt) |
202 |
Response.noContent() |
204 |
Response.notModified() |
304 |
Response.badRequest(inhalt) |
400 |
Response.unauthorized(inhalt) |
401 |
Response.forbidden(inhalt) |
403 |
Response.notFound(inhalt) |
404 |
Response.notAcceptable(inhalt) |
406 |
Response.serverError(inhalt) |
500 |
Response.status(code, inhalt) |
beliebig, z. B. 409 |
Response.redirect(url) |
302 (setzt Location) |
Response.seeOther(url) |
303 (setzt Location) |
new Response(code) |
leeres Objekt zum Selbstbefüllen |
Jedes Response lässt sich weiter einstellen — per Methode (verkettbar) oder
direkt über die Eigenschaften entity, mediaType, headers:
| Methode | Wirkung |
|---|---|
.type(mediaType) |
Content-Type setzen |
.body(inhalt) |
Inhalt setzen |
.header(name, wert) |
Antwort-Header setzen |
// Eigener Statuscode + JSON
return Response.status(409, { error: "Bereits vorhanden." });
// Weiterleitung
return Response.redirect("https://intranet.example/kunde/" + id);
// Selbst zusammengebaut
return new Response(200).body("<hello/>").type("application/xml").header("Cache-Control", "no-store");
Authentifizierung (pro Service)¶
Im Editor je Service unter Authentifizierung einstellbar:
| Modus | Aufrufer sendet | request.user im Handler |
|---|---|---|
| Bearer-Token | Authorization: Bearer <token> |
service:<name> |
| Basic (Benutzer/Passwort) | Authorization: Basic base64(benutzer:passwort) |
der Benutzername |
| Keine (offen) | — | anonymous |
Benutzername und Passwort bei Basic gehören zum Service — es sind keine KCForms-Benutzerkonten. Ein Bearer-Service ohne eingetragenen Token lehnt jeden Aufruf ab, ebenso ein Basic-Service, an dem Benutzer oder Passwort leer ist. Offen ist ein Service nur, wenn ausdrücklich Keine gewählt ist — eine unbekannte Einstellung sperrt ihn.
# Bearer
curl -H "Authorization: Bearer geheim123" \
"http://<server>/api/rest/service/kcforms/forms"
# Basic
curl -u benutzer:passwort \
"http://<server>/api/rest/service/intern/report"
Ein falscher oder fehlender Token liefert 401 mit passendem
WWW-Authenticate-Header. Token und Passwort werden in konstanter Zeit
verglichen (kein Rückschluss über Antwortzeiten).
Swagger-UI & OpenAPI¶
/api/rest/docs— eingebettete Swagger-UI über alle Services, mit?service=<name>für einen einzelnen. „Try it out" ruft direkt auf; für gesicherte Services oben rechts unter Authorize den Token eintragen./api/rest/openapi.json— das erzeugte OpenAPI-3-Dokument (ebenfalls mit?service=<name>).
Die Doku ist öffentlich (sie beschreibt nur die Schnittstelle), die Daten schützt die Authentifizierung des Service. Im Editor führt Swagger öffnen direkt zur Doku des gerade bearbeiteten Service.
documentation (info, schemas, tags) und die Angaben der Operationen
(summary, pathParameters, requestSchema …) fließen in das
OpenAPI-Dokument ein. $ref: "#/components/schemas/Customer" verweist auf ein
unter documentation.schemas definiertes Schema.
Die Skript-API im Handler¶
Ein Handler hat dieselben Objekte wie ein Formular-Skript, unter anderem:
| Objekt | Zweck |
|---|---|
forms |
Datensätze von Formularen lesen, anlegen, ändern, löschen |
db |
SQL gegen die KCForms-Datenbank oder externe Datenquellen |
http / ws |
ausgehende HTTP- / WebService-Aufrufe |
ldap |
LDAP-Abfragen |
mail |
E-Mail versenden |
fs |
Dateisystem-Sandbox |
csv / xml / zip |
CSV, XML, ZIP |
log |
Protokoll (log.info(…)) |
macros |
Makros auflösen |
ctx.user.name |
Aufrufer (wie request.user) |
ctx.system.id |
Kennung dieses Systems (z. B. DEV/QS/PROD) |
Kein Formular-Kontext
Ein REST-Service gehört zu keinem Formular: ctx.form ist leer,
ctx.formId fehlt, und alles, was ein Formular braucht (pdf.render(),
mail-Anhänge per addOutput …), steht hier nicht zur Verfügung. An
Formulardaten kommst du über forms oder db.
Zeitlimit
Jeder Aufruf hat dasselbe Zeitbudget wie ein Formular-Hook
(6 Sekunden, siehe Sandbox).
Dauert der Handler länger, antwortet KCForms mit 500
(„Script timeout …").
Beispiele¶
Formulardaten per forms¶
module.exports = {
path: "/bestellung",
operations: [
{
id: "get", path: "/{id}", method: "GET",
pathParameters: [ { name: "id", schema: { type: "integer" } } ],
handler: function () {
var rec = forms.get("bestellung", Number(pathParameters.id));
if (!rec) throw Response.notFound({ error: "Keine Bestellung " + pathParameters.id });
return rec; // inkl. Detail-Tabellen
}
},
{
id: "create", path: "", method: "POST",
handler: function () {
var daten = JSON.parse(body.asString());
var fehler = forms.validate("bestellung", daten);
if (fehler.length) throw Response.status(422, { errors: fehler });
return Response.created({ id: forms.create("bestellung", daten) });
}
}
]
};
forms.create legt den Datensatz ohne Formular-Hooks an; forms.validate
prüft vorher Pflichtfelder, Typen und Muster — Einzelheiten unter
forms.
KCForms-Daten lesen (mit db)¶
Ein Service, der Formulare und Benutzer aus KCForms liest — abgesichert per
Bearer-Token, weil er Daten preisgibt. Er liegt unter /kcdaten, damit er dem
mitgelieferten Beispiel kcforms nicht in die Quere kommt.
module.exports = {
path: "/kcdaten",
documentation: { info: { title: "KCForms-Daten", version: "1.0" } },
operations: [
{
id: "listForms", path: "/forms", method: "GET",
summary: "Formulare auflisten",
handler: function () {
return db.query("SELECT id, title, catalog FROM form_definition ORDER BY id");
}
},
{
id: "getForm", path: "/forms/{id}", method: "GET",
pathParameters: [ { name: "id", schema: { type: "string" } } ],
handler: function () {
var f = db.queryOne(
"SELECT id, title, version FROM form_definition WHERE id = ?",
pathParameters.id);
if (!f) throw Response.notFound({ error: "Kein Formular '" + pathParameters.id + "'." });
return f;
}
},
{
id: "userLogins", path: "/users/logins", method: "GET",
summary: "Anmeldenamen auflisten",
handler: function () {
return db.query("SELECT login_name AS login FROM users ORDER BY login_name")
.map(function (r) { return r.login; }); // ["admin", …]
}
},
{
id: "userList", path: "/users", method: "GET",
summary: "Alle Benutzer (ohne Passwort)",
handler: function () {
// Nur die Spalten, die nach außen dürfen — nie password/salt.
return db.query(
"SELECT id_user AS id, login_name AS login, first_name AS firstName, " +
"last_name AS lastName, email, locale, last_login AS lastLogin " +
"FROM users ORDER BY login_name");
}
}
]
};
Die Zeilen aus db.query kannst du direkt zurückgeben — KCForms macht daraus
JSON. Spaltennamen kommen klein geschrieben an; mit AS vergibst du
sprechende Schlüssel.
Sensible Spalten nie ausliefern
Die Tabelle users enthält password und salt. Selektiere sie
nicht (SELECT * vermeiden!) und sichere Services mit Personendaten
per Bearer-Token oder Basic ab — nie mit „Keine".
JSON.stringify auf Datenbankzeilen
rows.length, rows[i].spalte und Array-Methoden wie map/filter
funktionieren auf dem Ergebnis von db.query. JSON.stringify(rows)
liefert dagegen leere Objekte ([{},{}]), weil die Zeilen keine
JavaScript-Objekte sind. Zum Zurückgeben brauchst du das nicht; zum
Protokollieren die Werte einzeln ausgeben.
CRUD (In-Memory)¶
var customers = [
{ id: "1", lastName: "Mustermann", firstName: "Erika" },
{ id: "2", lastName: "Mustermann", firstName: "Max" }
];
function findById(id) {
for (var i = 0; i < customers.length; i++) if (customers[i].id === id) return customers[i];
return null;
}
module.exports = {
path: "/customer",
documentation: {
info: { title: "Customer-Service", version: "1.0" },
schemas: {
Customer: {
type: "object",
properties: {
id: { type: "string" },
lastName: { type: "string" },
firstName: { type: "string" }
},
required: [ "id" ],
example: { id: "1", lastName: "Mustermann", firstName: "Erika" }
}
}
},
operations: [
{
id: "list", path: "", method: "GET",
responseSchema: { type: "array", items: { "$ref": "#/components/schemas/Customer" } },
handler: function () { return customers; }
},
{
id: "get", path: "/{id}", method: "GET",
pathParameters: [ { name: "id", schema: { type: "string" } } ],
responseSchema: { "$ref": "#/components/schemas/Customer" },
handler: function () {
var c = findById(pathParameters.id);
if (!c) throw Response.notFound({ error: "Kein Kunde " + pathParameters.id });
return c;
}
},
{
id: "add", path: "", method: "POST",
requestSchema: { "$ref": "#/components/schemas/Customer" },
responseSchema: { "$ref": "#/components/schemas/Customer" },
handler: function () {
var data = JSON.parse(body.asString());
if (!data.id) throw Response.badRequest({ error: "id ist Pflicht." });
if (findById(data.id)) throw Response.status(409, { error: "id vergeben." });
customers.push(data);
return Response.created(data);
}
}
]
};
Jeder Aufruf beginnt von vorn
Jeder Aufruf läuft in einem frischen Skript-Kontext — customers wird
bei jedem Request neu aufgebaut, add bleibt nicht erhalten. Dauerhaft
speichern geht über forms oder db. Aus demselben Grund ist this im
Handler nicht belegt; greife wie oben über Variablen des Moduls zu.
Fehler & Sonderstatus¶
operations: [
{ id: "s", path: "/throw", method: "GET",
handler: function () { throw "kaputt"; } }, // 500, {"error":"kaputt"}
{ id: "o", path: "/object", method: "GET",
handler: function () { throw { message: "kaputt", code: 4711 }; } }, // 500, strukturiert
{ id: "t", path: "/konflikt", method: "GET",
handler: function () { return Response.status(409, { hint: "schon da" }); } } // 409
]
Health-Check für das Portal¶
KCForms bringt einen eingebauten Health-Service mit — er ist immer vorhanden (auch ohne Beispiele) und öffentlich (ohne Token), damit ein umgebendes Portal oder ein Monitoring jederzeit „läuft KCForms?" prüfen kann. Er gibt keine sensiblen Daten zurück.
| Endpunkt | Zweck | Antwort |
|---|---|---|
GET /api/rest/service/health |
Liveness — antwortet KCForms? | 200 { "status": "UP", … } |
GET /api/rest/service/health/ready |
Readiness — auch die Datenbank erreichbar? | 200 bereit / 503 nicht bereit |
# Liveness (200, solange die Anwendung Anfragen bedient)
curl http://<server>/api/rest/service/health
# {"status":"UP","service":"KCForms","time":"2026-07-07T09:12:34.567Z"}
# Readiness (prüft zusätzlich die Datenbank)
curl -i http://<server>/api/rest/service/health/ready
# HTTP/1.1 200 {"status":"UP","db":"UP","service":"KCForms","time":"…"}
# Bei DB-Ausfall: HTTP/1.1 503 {"status":"DOWN","db":"DOWN",…}
Für einen schnellen Ping ohne Datenbankzugriff /health; soll die Anbindung
erst „grün" melden, wenn auch die Datenbank erreichbar ist, /health/ready
(den Status 503 als „nicht bereit" werten).
Der Service heißt in der Verwaltung healthCheck und lässt sich dort wie
jeder andere anpassen, z. B. um weitere Prüfungen:
module.exports = {
path: "/health",
documentation: { info: { title: "Health-Check", version: "1.0" } },
operations: [
{
id: "liveness", path: "", method: "GET",
summary: "Up & Running?",
handler: function () {
return { status: "UP", service: "KCForms", time: new Date().toISOString() };
}
},
{
id: "readiness", path: "/ready", method: "GET",
summary: "Voll betriebsbereit (inkl. Datenbank)?",
handler: function () {
try {
db.scalar("SELECT 1");
return { status: "UP", db: "UP", service: "KCForms", time: new Date().toISOString() };
} catch (e) {
return Response.status(503, { status: "DOWN", db: "DOWN", service: "KCForms", error: String(e) });
}
}
}
]
};
SELECT 1 auf Oracle
Läuft KCForms auf Oracle, braucht die Prüfabfrage eine Tabelle:
SELECT 1 FROM DUAL.
Verwaltung¶
System-Administration → Schnittstellen & Verteilung → REST-Services:
- Liste mit Name, Basis-Pfad, Authentifizierung, Zahl der Operationen, Markierung ⚠ Fehler, wenn sich der Quelltext nicht laden lässt, und Beispiel bei mitgelieferten Services.
- Editor mit Name, Authentifizierung, Beschreibung, Zugangsdaten, dem Modul-Quelltext und dem Schalter freigeschaltet.
- Prüfen — lädt den Quelltext probeweise, ohne zu speichern, und zeigt Pfad und Operationen oder den Fehler.
- Anlegen / Speichern — der Service ist sofort mit dem neuen Stand erreichbar.
- Swagger öffnen — springt in die API-Doku dieses Service.
- Löschen.
Token und Passwort erscheinen in der Oberfläche maskiert (********); lässt
du die Maske stehen, bleibt der gespeicherte Wert erhalten.
Ein Service ohne Haken bei freigeschaltet verschwindet aus Aufruf und Swagger, bleibt aber gespeichert.
Beispiel-Services¶
Die vier mitgelieferten Beispiele — hello, customer, errors und
kcforms — werden nur eingespielt, wenn die Beispiele eingeschaltet sind
(Einstellung KCFORMS_SEED_EXAMPLES=true). Danach stehen sie in der
Liste und lassen sich wie jeder andere Service bearbeiten oder löschen. Der
Service kcforms ist per Bearer-Token geschützt. Das Token wird bei der
Installation zufällig erzeugt und nirgends angezeigt — wer den Service
ausprobieren will, trägt in der Liste unter Token ein eigenes ein und
benutzt genau dieses beim Aufruf (in Swagger unter Authorize).
Ältere Installationen
Vor dem Stand vom 17.10.2026 trug der Service ein fest eingebautes Token, das in diesem Handbuch stand. Beim nächsten Start wird es automatisch durch ein zufälliges ersetzt; ein selbst eingetragenes Token bleibt unverändert.
Sicherheit auf einen Blick¶
- Verwaltung nur für Administratoren; der Service-Code läuft mit vollen Server-Rechten.
- Absicherung je Service (Bearer/Basic/keine) — wähle nicht „Keine" für Services, die Daten preisgeben oder etwas verändern.
- Der
Authorization-Header wird dem Handler nicht durchgereicht. - Jeder Aufruf hat dasselbe Zeitbudget wie ein Formular-Hook.
- Die API-Doku (
/api/rest/docs,openapi.json) ist öffentlich — sie beschreibt nur die Schnittstelle; die Daten schützt die Authentifizierung.