Zum Inhalt

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:

curl "http://<server>/api/rest/service/hello?name=Anna"
# {"message":"Hallo, Anna!"}

<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

handler: function () { return { ok: true, zeit: new Date().toISOString() }; }
  • Objekt / Array → JSON (application/json)
  • String → Text (text/plain, oder der produces-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

handler: function () {
  return Response.ok("id;name\n1;Anna").type("text/csv; charset=UTF-8");
}

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" → 500 mit { "error": "Text" }
  • throw new Error("Text") → 500 mit { "error": "Text" }; eigene Felder am Fehler (e.code = "K1") kommen mit
  • throw { … } → 500 mit genau diesem Objekt
  • throw 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.