Bringing the abap2UI5 concept to CAP/Node.js.
Important
Everything in this project is generated automatically. The entire codebase, all documentation, and the web version were created by AI (Claude) and by an automated sync pipeline that mirrors and transpiles the upstream abap2UI5 sources — nothing here is hand-written. See builder-abap2UI5-js and builder-cap2UI5 for how the pipeline works. Review and test before relying on it.
- XML View Generation - Create UI5 views programmatically in your backend
- Data Binding & Exchange - Seamless two-way data binding between frontend and backend
- Session Management - Built-in persistence and session handling (optional)
- Security
- Speed
Prerequisites: Node.js ≥ 22 (see .nvmrc). The whole stack runs offline —
the UI5 runtime is served locally from the pinned openui5-dist dependency,
and CAP deploys an in-memory SQLite database automatically on startup.
npm install
# start the server (restarts on file changes)
npx cds watch
# or: start and open the app in the browser right away
npm run watch-z2ui5The server listens on http://localhost:4004:
| URL | What you get |
|---|---|
http://localhost:4004/z2ui5/webapp/index.html |
the app — without a parameter the startup app is shown |
http://localhost:4004/z2ui5/webapp/index.html?app_start=z2ui5_cl_app_hello_world |
start a specific app class via the app_start parameter (works for every sample, e.g. z2ui5_cl_demo_app_001) |
http://localhost:4004/index.html |
the minimal starter page — one roundtrip through all three layers |
http://localhost:4004/rest/root/z2ui5 |
the roundtrip endpoint the frontend talks to |
http://localhost:4004/odata/v4/admin/z2ui5_t_01 |
the draft table (session persistence) via OData |
For a one-off run without file watching use npm start (cds-serve).
The base is the starting point of cap2UI5: a small but complete CAP project that works on its own, before any generated code is added. It brings the same basic setup as abap2UI5 — a frontend that talks JSON to a single http endpoint, and a draft table that persists the app state between roundtrips — reduced to the minimum:
| Layer | Where | What it does |
|---|---|---|
| (1) mini frontend | app/index.html |
a single self-contained UI5 page that POSTs one roundtrip, renders the returned view XML and shows the draft id + row count |
| (2) http service | srv/z2ui5-service.cds / .js + srv/server.js |
rootService.z2ui5 — the REST action at POST /rest/root/z2ui5 (plus GET bootstrap HTML), same wire format as the abap2UI5 ICF endpoint |
| (3) persistence | db/schema.cds |
entity cap2ui5.z2ui5_t_01 — one draft row per roundtrip, chained via id_prev (the abap2UI5 table Z2UI5_T_01) |
The base is hand-maintained in
builder-cap2UI5's src/
and published 1:1 into this repository. The framework itself — the engine, the
transpiled classes, the z2ui5 frontend and the ~180 bundled samples — lives in
the platform-neutral core package (core/, vendored here and
linked as the npm dependency abap2UI5 via file:./core) and is generated
by the sync pipeline in
builder-abap2UI5-js. All
commands below work in both places: in builder-cap2UI5's src/ (source only)
and in this repository (base + the vendored core + the generated webapp
overlay).
A regular CAP project with the standard layout:
base/
├── app/ # UI content
│ └── index.html # the mini frontend (starter page)
├── db/ # domain model
│ └── schema.cds # cap2ui5.z2ui5_t_01 — the draft table
├── srv/ # services and implementation
│ ├── z2ui5-service.cds # AdminService (OData) + rootService (REST)
│ ├── z2ui5-service.js # wires POST /rest/root/z2ui5 → the engine roundtrip
│ ├── server.js # GET bootstrap HTML, /resources, draft store + app dir ports
│ ├── app/ # custom apps (z2ui5_cl_app_read_odata, your own)
│ └── external/ # imported remote service model (Northwind)
├── test/ # jest: starter integration test + view builder test
└── package.json # @sap/cds ^10, "abap2UI5": file-link to the vendored core/, Node ≥ 22
The z2ui5 runtime itself is not hand-written in this project — it comes from
the vendored core package (require("abap2UI5/engine")), which srv/server.js
wires to CAP: the draft store port to the CDS entity cap2ui5.z2ui5_t_01,
the app-discovery port to this project's srv/app/.
There is nothing to configure: the UI5 runtime is served locally from the
pinned openui5-dist dependency (works offline) and CAP deploys an
in-memory SQLite database automatically on startup.
npm install
npx cds watch # → http://localhost:4004/index.htmlOpen http://localhost:4004/index.html:
every click on POST /rest/root/z2ui5 runs one full roundtrip — the
backend app class builds the view, the response's draft id is the new row
key in cap2ui5.z2ui5_t_01, and the row count (read back via the
AdminService OData endpoint) increases by one.
Or exercise the service from the command line:
curl -s -X POST http://localhost:4004/rest/root/z2ui5 \
-H "Content-Type: application/json" \
-d '{"value":{"S_FRONT":{"ORIGIN":"http://localhost:4004","PATHNAME":"/index.html","SEARCH":"","HASH":""}}}'
curl -s http://localhost:4004/odata/v4/admin/z2ui5_t_01/\$countnpm testruns the jest suite: test/starter.test.js boots the
real server via cds.test() and asserts all three layers end-to-end
(starter page + bootstrap HTML, roundtrip returning view XML, draft row
persisted per roundtrip), and test/z2ui5_cl_xml_view.test.js
covers the view builder.
This project deliberately ships without authentication: xs-security.json
declares no scopes, attributes or role templates, and package.json sets
cds.requires.[production].auth: false, so every endpoint (the z2ui5
roundtrip, the OData admin service and the draft table) is open. That is
intentional for the demo/sample character of this repository. For a real
BTP deployment, configure authentication before going productive: define
scopes and role templates in xs-security.json, remove the
[production].auth: false override and wire the CAP services to XSUAA
(kind: xsuaa) — see the
CAP authorization guide.
App classes can be transpiled automatically from the abap2UI5 ABAP sources — the transpiler and all other dev tooling live in builder-abap2UI5-js.
All samples demonstrate complete view definition and data exchange handled entirely by the CAP server, using the same and static frontend from abap2UI5.
Each app is a single .js file whose basename matches the class name it
exports (module.exports). Put your own apps into srv/app/ (scanned
automatically when resolving ?app_start=<class>, see the
custom apps README) or into any folder registered via
Z2UI5_APP_DIRS / require("abap2UI5/register-apps")(dir) — see the
discovery API. The
bundled samples and the framework classes live in the vendored core package
(core/), which is owned by the sync pipeline.
// z2ui5_cl_app_hello_world.js — ships with the core package
const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
class z2ui5_cl_app_hello_world extends z2ui5_if_app {
name = ``;
async main(client) {
if (client.check_on_init()) {
const view = z2ui5_cl_xml_view.factory()
.shell()
.page(`abap2UI5 - Hello World`)
.simple_form({ editable: true })
.content(`form`)
.title({ ns: `core`, text: `Enter a value and send it to the server...` })
.label(`Name`)
.input(client._bind_edit(this.name))
.button({ text: `Send`, press: client._event(`BUTTON_POST`) });
client.view_display(view.stringify());
} else if (client.check_on_event(`BUTTON_POST`)) {
client.message_box_display(`Your name is ${this.name}`);
}
}
}
module.exports = z2ui5_cl_app_hello_world; "northwind": {
"kind": "odata-v2",
"model": "srv/external/northwind",
"credentials": {
"url": "https://services.odata.org/V2/Northwind/Northwind.svc/"
}
}// srv/app/z2ui5_cl_app_read_odata.js — ships with the project
const cds = require("@sap/cds");
const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
class z2ui5_cl_app_read_odata extends z2ui5_if_app {
customers = [];
async main(client) {
if (client.check_on_init()) {
// The remote demo service may be unreachable (offline, proxy, service
// down) — show the error in the UI instead of breaking the app init.
try {
const northwind = await cds.connect.to(`northwind`);
this.customers = await northwind.run(
SELECT.from(`Customers`).columns(`CompanyName`, `ContactName`).limit(20)
);
} catch (e) {
const view = z2ui5_cl_xml_view.factory();
view.shell()
.page(`abap2UI5 - Table with Data Fetched via Remote OData`)
.message_strip({
text: `Remote Northwind service not reachable: ${e.message}`,
type: `Error`,
showicon: true,
class: `sapUiSmallMargin`,
});
client.view_display(view.stringify());
client.message_box_display(`Remote Northwind service not reachable: ${e.message}`, `error`);
return;
}
const view = z2ui5_cl_xml_view.factory();
const tab = view.shell()
.page(`abap2UI5 - Table with Data Fetched via Remote OData`)
.table({ items: client._bind_edit(this.customers) });
tab.columns()
.column().text(`CompanyName`).get_parent()
.column().text(`ContactName`);
tab.items()
.column_list_item()
.cells()
.input({ value: `{COMPANYNAME}`, enabled: true })
.input({ value: `{CONTACTNAME}`, enabled: true });
client.view_display(view.stringify());
}
}
}
module.exports = z2ui5_cl_app_read_odata;Note: the client model uppercases all property names — the cells bind
{COMPANYNAME}, not {CompanyName}.
<mvc:View
controllerName="Quickstart.App"
displayBlock="true"
xmlns:mvc="sap.ui.core.mvc"
xmlns:l="sap.ui.layout"
xmlns:core="sap.ui.core"
xmlns:tnt="sap.tnt"
xmlns="sap.m">
<App id="app">
<Page title="Create Enterprise-ready Web Apps with Ease">
<l:BlockLayout background="Light">
<l:BlockLayoutRow>
<l:BlockLayoutCell>
<core:Icon color="#1873B4" src="sap-icon://sap-ui5" size="5rem" class="sapUiSmallMarginBottom" width="100%"/>
<Title level="H1" titleStyle="H1" text="This is UI5!" width="100%" textAlign="Center"/>
</l:BlockLayoutCell>
</l:BlockLayoutRow>
<l:BlockLayoutRow>
<l:BlockLayoutCell>
<FlexBox items="{/features}" justifyContent="Center" wrap="Wrap" class="sapUiSmallMarginBottom">
<tnt:InfoLabel text="{}" class="sapUiSmallMarginTop sapUiSmallMarginEnd"/>
</FlexBox>
</l:BlockLayoutCell>
</l:BlockLayoutRow>
<l:BlockLayoutRow>
<l:BlockLayoutCell>
<Panel headerText="Are you ready?" expandable="true">
<Switch change=".onChange" customTextOn="yes" customTextOff="no"/>
<l:HorizontalLayout id="ready" visible="false" class="sapUiSmallMargin">
<Text text="Ok, let's get you started!" class="sapUiTinyMarginEnd"/>
<Link text="Learn more" href="https://openui5.hana.ondemand.com/"/>
</l:HorizontalLayout>
</Panel>
</l:BlockLayoutCell>
</l:BlockLayoutRow>
</l:BlockLayout>
</Page>
</App>
</mvc:View>// z2ui5_cl_app_read_view.js — ships with the core package
const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
class z2ui5_cl_app_read_view extends z2ui5_if_app {
async main(client) {
const fs = require("fs");
const path = require("path");
const viewPath = path.join(__dirname, "View1.view.xml");
const viewContent = fs.readFileSync(viewPath, "utf8");
client.view_display(viewContent);
}
}
module.exports = z2ui5_cl_app_read_view;Contributions are welcome! Feel free to fork the project, submit issues, or create pull requests.
This project is licensed under the MIT License.

