SAP Cloud Application Programming Model
The recommended open-source framework for building cloud-native enterprise applications and services on SAP BTP. CDS-first, convention-over-configuration, automatic OData V4 generation, Node.js and Java runtimes.
What is SAP CAP?
The SAP Cloud Application Programming Model (CAP) is an open-source, convention-over-configuration framework for building cloud-native business services and full-stack applications on SAP BTP. Its foundation is CDS (Core Data Services) — a declarative, schema-first domain language for defining data models, service interfaces, and UI annotations. From a single CDS model, CAP automatically generates OData V4 endpoints, handles database persistence, enforces authorisation, and emits CloudEvents.
CAP supports two runtime stacks: Node.js (recommended default — fast iteration, hot-reloading, lighter footprint) and Java (preferred for enterprise Java teams, Spring Boot-based, fully equivalent CDS feature set). Both runtimes share the same CDS data model, service definitions, and annotation vocabulary.
CAP is the strategic development model for Clean Core extensions — all side-by-side extensions to SAP S/4HANA Public Cloud, RISE, and GROW with SAP are built on BTP using CAP, consuming S/4HANA released OData V4 APIs via the BTP Destination Service.
Quick Facts
- License
- Open Source — Apache 2.0
- Package
- @sap/cds · @sap/cds-dk
- Runtimes
- Node.js 20+ · Java 21+ (Spring)
- Language
- CDS (Core Data Services)
- Protocol
- OData V4 · REST · GraphQL (plugin)
- DB (prod)
- SAP HANA Cloud (HDI)
- DB (local)
- SQLite (in-memory)
- Deployment
- MTA → CF · Kyma (Helm)
- Multitenancy
- @sap/cds-mtxs sidecar
- Auth
- IAS + XSUAA (JWT)
Video Tutorials
Official SAP channel walkthroughs — click any card to play
21:05CAP Node.js — Build a Service in 20 Min
35:22CDS Data Modelling Deep Dive
28:47CAP Multitenancy with MTX Extension
19:30CAP Java — Getting Started
SAP CAP — Architectural Layers
CDS Build Pipeline
The CDS compiler transforms your .cds source files into multiple artefacts: a CSN (Core Schema Notation) JSON manifest, an EDMX OData service metadata document, HANA HDI DDL files for the production database, and SQLite DDL for local development. Running cds build --production prepares the full output ready for MTA or Helm packaging.
CDS Entity Definitions — Schema with Aspects
1// db/schema.cds — CAP data model using CDS aspects
2using { managed, cuid } from '@sap/cds/common';
3using { Currency } from '@sap/cds/common';
4
5namespace com.acme.bookshop;
6
7// ─── Core Entities ────────────────────────────────────────────────────────────
8entity Books : cuid, managed {
9 title : localized String(111) @mandatory;
10 author : Association to Authors;
11 price : Decimal(9,2);
12 currency : Currency;
13 stock : Integer default 0;
14 description : localized String(1111);
15 active : Boolean default true;
16}
17
18entity Authors : cuid, managed {
19 name : String(111) @mandatory;
20 dateOfBirth : Date;
21 nationality : String(60);
22 // Backlink composition — Authors own their Books
23 books : Composition of many Books on books.author = $self;
24}
25
26entity Orders : cuid, managed {
27 OrderNo : String @readonly @Core.Computed;
28 buyer : String @mandatory;
29 status : String(20) enum {
30 open = 'Open';
31 confirmed = 'Confirmed';
32 cancelled = 'Cancelled';
33 } default 'open';
34 items : Composition of many OrderItems on items.order = $self;
35}
36
37entity OrderItems : cuid {
38 order : Association to Orders;
39 book : Association to Books;
40 quantity : Integer @assert.range: [1, 1000] @mandatory;
41 netAmount : Decimal(9,2);
42}
43
44// ─── Reusable Aspect — Soft Delete ───────────────────────────────────────────
45aspect SoftDelete {
46 isDeleted : Boolean default false;
47 deletedAt : Timestamp @cds.on.delete: $now;
48 deletedBy : String @cds.on.delete: $user;
49}cuid aspectAdds a generated UUID primary key. Applied as `: cuid` mixin — no manual key field needed.
managed aspectAdds createdAt, createdBy, modifiedAt, modifiedBy fields. Auto-populated by CAP from the JWT context.
localizedMarks a String field for automatic localisation. CAP generates a companion _texts table for translated values.
CompositionDeclares a parent-child ownership relationship. Deep insertions and cascading deletes are handled by CAP.
AssociationDeclares a navigable relationship (foreign key). No automatic cascade — applications control the lifecycle.
@assert.rangeBuilt-in CAP annotation for server-side range validation. Raises a 400 error if the value is outside the range.
CDS Service Definition — Projections and Authorisation
1// srv/catalog-service.cds — service definition with authorisation annotations
2using { com.acme.bookshop as db } from '../db/schema';
3
4// ─── Catalog Service — exposed at /catalog ───────────────────────────────────
5@path: '/catalog'
6service CatalogService {
7
8 // Public read — no authentication required for browsing
9 @readonly
10 entity Books as projection on db.Books {
11 *,
12 author.name as authorName,
13 } excluding { createdBy, modifiedBy, isDeleted };
14
15 // Authenticated write — restricted to CatalogEditor role collection
16 @restrict: [
17 { grant: ['READ'], to: 'authenticated-user' },
18 { grant: ['CREATE','UPDATE','DELETE'], to: 'CatalogEditor' }
19 ]
20 entity BooksAdmin as projection on db.Books;
21
22 // Orders — restricted read and write
23 @restrict: [
24 { grant: ['READ'], to: ['OrdersViewer', 'OrdersAdmin'] },
25 { grant: ['CREATE'], to: 'authenticated-user' },
26 { grant: ['UPDATE','DELETE'], to: 'OrdersAdmin' }
27 ]
28 entity Orders as projection on db.Orders;
29
30 entity OrderItems as projection on db.OrderItems;
31
32 // ── Custom Action: Submit an order (changes status Open → Confirmed) ───
33 @requires: 'authenticated-user'
34 action submitOrder (orderId: UUID) returns {
35 message : String;
36 newStatus : String;
37 };
38
39 // ── Custom Function: Top-selling books (GET, read-only) ────────────────
40 @readonly
41 @requires: 'authenticated-user'
42 function topBooks (count: Integer default 10) returns array of Books;
43}@requires to require any authenticated user or a specific scope. Use @restrict for fine-grained per-operation role control — grant specific HTTP verbs (READ, CREATE, UPDATE, DELETE) to specific role names. Role names in @restrict must match role template names in xs-security.json.Event Handlers
CAP processes every OData or REST request through a three-phase pipeline: before (validation and enrichment), on (main business logic — reads, writes, or delegation to remote services), and after (response post-processing and computed fields). Handlers are registered via this.before / this.on / this.after inside the cds.service.impl callback.
1// srv/catalog-handler.js — Node.js event handler implementation
2const cds = require('@sap/cds')
3
4module.exports = cds.service.impl(async function (srv) {
5
6 const { Books, Orders, OrderItems } = this.entities
7
8 // ─── BEFORE: Validate stock before order items creation ───────────────────
9 this.before('CREATE', OrderItems, async req => {
10 const { book_ID, quantity } = req.data
11
12 // Reject with 400 if required fields are missing
13 if (!book_ID) return req.reject(400, 'Book reference is required')
14 if (!quantity) return req.reject(400, 'Quantity must be at least 1')
15
16 // Check stock availability
17 const book = await SELECT.one.from(Books)
18 .where({ ID: book_ID })
19 .columns('stock', 'title', 'active')
20
21 if (!book) return req.reject(404, `Book ${book_ID} not found`)
22 if (!book.active) return req.reject(422, `"${book.title}" is no longer available`)
23 if (book.stock < quantity)
24 return req.reject(409, `Insufficient stock for "${book.title}" — only ${book.stock} available`)
25 })
26
27 // ─── ON: Auto-generate OrderNo and persist ────────────────────────────────
28 this.on('CREATE', Orders, async req => {
29 // Assign a sequential order number before persisting
30 req.data.OrderNo = `ORD-${new Date().getFullYear()}-${Date.now().toString(36).toUpperCase()}`
31 return this.emit('CREATE', Orders, req) // continue default DB handler
32 })
33
34 // ─── AFTER: Enrich Books read response with computed availability ─────────
35 this.after('READ', Books, books => {
36 const rows = Array.isArray(books) ? books : [books]
37 for (const book of rows) {
38 book.isAvailable = (book.stock ?? 0) > 0
39 }
40 })
41
42 // ─── CUSTOM ACTION: Submit an order ───────────────────────────────────────
43 this.on('submitOrder', async req => {
44 const { orderId } = req.data
45
46 // Verify the order belongs to the requesting user
47 const order = await SELECT.one.from(Orders)
48 .where({ ID: orderId, buyer: req.user.id })
49
50 if (!order) return req.reject(404, 'Order not found or access denied')
51 if (order.status !== 'open')
52 return req.reject(422, `Order is already ${order.status}`)
53
54 // Transition to Confirmed and decrement stock atomically
55 await UPDATE(Orders).set({ status: 'Confirmed' }).where({ ID: orderId })
56
57 const items = await SELECT.from(OrderItems).where({ order_ID: orderId })
58 for (const item of items) {
59 await UPDATE(Books).with({ stock: { '-=': item.quantity } })
60 .where({ ID: item.book_ID })
61 }
62
63 // Emit domain event — picked up by SAP Event Mesh if configured
64 await this.emit('OrderConfirmed', { orderId, buyer: req.user.id })
65
66 return { message: 'Order confirmed', newStatus: 'Confirmed' }
67 })
68
69 // ─── CUSTOM FUNCTION: Top-selling books (read-only) ───────────────────────
70 this.on('topBooks', async req => {
71 const { count = 10 } = req.data
72 return SELECT.from(Books)
73 .orderBy({ stock: 'asc' }) // low stock = popular
74 .limit(count)
75 })
76
77})OData V4 and API Patterns
CAP automatically generates OData V4 endpoints for every entity exposed in a CDS service — including $metadata, $filter, $expand, $orderby, and $batch. No additional configuration is needed. Custom actions (POST, side-effectful) and functions (GET, read-only) extend the OData surface beyond CRUD. A REST adapter is also available for non-OData consumers.
GET /catalog/BooksCollection read — supports $filter, $select, $expand, $orderby, $top, $skip
GET /catalog/Books(ID)Single entity read by key — $expand for associations
POST /catalog/BooksDeep insert — nested OrderItems composition inserted atomically
POST /catalog/submitOrderBound action — body is { orderId }; handler logic runs server-side
GET /catalog/topBooks(count=5)Unbound function — GET, no side effects, result is array of Books
GET /catalog/$metadataOData V4 EDMX metadata document — consumed by Fiori Elements
POST /catalog/$batchOData V4 batch — multiple requests in one HTTP call, atomic or not
GET /catalog/Books?$filter=stock gt 0Server-side filtering — CAP translates to SQL WHERE clause
Authentication and Authorisation
CAP integrates with the SAP BTP Authorization and Trust Management Service (XSUAA) for JWT-based authentication and role enforcement. Every incoming request must carry a valid JWT Bearer token issued by the IAS-configured XSUAA instance. CAP validates the token, extracts req.user, and evaluates @restrict annotations automatically — no manual token parsing needed.
1// xs-security.json — XSUAA security descriptor for CAP application
2{
3 "xsappname": "bookshop-cap",
4 "tenant-mode": "dedicated",
5 "description": "CAP Bookshop application — role-based access control",
6 "scopes": [
7 {
8 "name": "$XSAPPNAME.CatalogEditor",
9 "description": "Create, update, and delete books in the catalogue"
10 },
11 {
12 "name": "$XSAPPNAME.OrdersViewer",
13 "description": "Read-only access to all orders"
14 },
15 {
16 "name": "$XSAPPNAME.OrdersAdmin",
17 "description": "Full order management — update, cancel, confirm"
18 }
19 ],
20 "attributes": [],
21 "role-templates": [
22 {
23 "name": "CatalogEditor",
24 "description": "Manage book catalogue",
25 "scope-references": [ "$XSAPPNAME.CatalogEditor" ]
26 },
27 {
28 "name": "OrdersViewer",
29 "description": "Read-only access to orders",
30 "scope-references": [ "$XSAPPNAME.OrdersViewer" ]
31 },
32 {
33 "name": "OrdersAdmin",
34 "description": "Full order lifecycle management",
35 "scope-references": [ "$XSAPPNAME.OrdersViewer", "$XSAPPNAME.OrdersAdmin" ]
36 }
37 ],
38 "role-collections": [
39 {
40 "name": "Bookshop Catalog Editor",
41 "role-template-references": [ "$XSAPPNAME.CatalogEditor" ]
42 },
43 {
44 "name": "Bookshop Order Admin",
45 "role-template-references": [ "$XSAPPNAME.OrdersAdmin" ]
46 }
47 ],
48 "oauth2-configuration": {
49 "token-validity": 43200,
50 "redirect-uris": [
51 "https://*.cfapps.eu10.hana.ondemand.com/**",
52 "https://*.cfapps.ae1.hana.ondemand.com/**"
53 ]
54 }
55}req.user.id // → user principal (email or user ID)
req.user.tenant // → tenant ID (for multitenant apps)
req.user.is('CatalogEditor') // → true if role assigned
req.user.attr.country // → custom attribute from IAS// Reject if the user is not an admin
if (!req.user.is('OrdersAdmin')) {
return req.reject(403, 'Insufficient privileges')
}
// Or use req.error to collect validation errors
req.error(403, 'Access denied', 'auth/forbidden')Multitenancy and SaaS
CAP provides built-in multitenancy support via the @sap/cds-mtxs (Multitenancy and Extensibility Sidecar). Each tenant gets an isolated SAP HANA Cloud HDI container (its own database schema) provisioned by the MTX sidecar when the tenant subscribes via the SAP SaaS Provisioning Service. The provider application runs once on BTP; tenant context is extracted from the tenantId claim in the JWT Bearer token.
1// srv/mtx-handler.js — SaaS tenant lifecycle callbacks using @sap/cds-mtxs
2const cds = require('@sap/cds')
3
4// MTX lifecycle is handled by the sidecar — this shows custom callbacks
5cds.on('served', async () => {
6 const { 'cds.xt.SaasProvisioningService': sps } = cds.services
7
8 if (sps) {
9 // ── On Tenant Subscribe: run custom onboarding logic ─────────────────
10 sps.before('subscribe', async req => {
11 const { tenantId, subscriptionAppName } = req.data
12 cds.log('mtx').info(`Tenant ${tenantId} subscribing to ${subscriptionAppName}`)
13 // Custom: create initial seed data for new tenant
14 })
15
16 // ── On Tenant Upgrade: apply pending schema migrations ────────────────
17 sps.after('upgrade', async req => {
18 const { tenantId } = req.data
19 cds.log('mtx').info(`Schema migration complete for tenant ${tenantId}`)
20 })
21
22 // ── On Tenant Unsubscribe: cleanup tenant data ────────────────────────
23 sps.before('unsubscribe', async req => {
24 const { tenantId } = req.data
25 cds.log('mtx').warn(`Tenant ${tenantId} is unsubscribing — archiving data`)
26 // Custom: export tenant data before HDI container decommission
27 })
28 }
29})// package.json — add mtxs
{
"cds": {
"requires": {
"multitenancy": true
}
}
}# mta.yaml — add MTX module
- name: bookshop-mtx
type: nodejs
path: mtx/sidecar
requires:
- name: bookshop-db
- name: bookshop-auth// CAP automatically uses the // tenant's HDI container // No manual schema switching needed const books = await SELECT .from(Books) // → reads from tenant's schema
External Services and S/4HANA Integration
CAP supports first-class integration with external OData V4 services (including S/4HANA released APIs) via cds import. This command downloads the EDMX metadata from a live service or a file and generates typed CDS stubs. At runtime, CAP routes queries to the remote service transparently via the BTP Destination Service — no hand-written HTTP calls needed. Three patterns are supported: transparent delegation, data enrichment (mashup), and outbound writes.
| Pattern | Description | Use when |
|---|---|---|
| Transparent Delegation | CAP passes req.query directly to the remote service — response returned as-is | Simple proxy — no local data involved |
| Mashup / Enrichment | Merge remote S/4 data with local HANA data in a single response | Adding local metadata (notes, flags) to S/4 entities |
| Cached Replication | CAP replicates remote data into local HANA via events — reads always local | High-read, low-write remote entities (e.g. cost centres) |
| Outbound Write | CAP forwards CREATE/UPDATE to S/4HANA via the external service connection | Write-back to S/4HANA from BTP extension |
| Mocked External Service | cds.env.requires.<svc>.kind = "odata-v4" replaced by "mock" in tests | Unit tests without live S/4HANA connection |
1// srv/s4-integration.cds — import S/4HANA external OData service
2// Run: cds import API_SALES_ORDER_SRV --from https://<s4-host>/sap/opu/odata/sap/API_SALES_ORDER_SRV
3
4using { API_SALES_ORDER_SRV as s4 } from './external/API_SALES_ORDER_SRV';
5
6// ─── Projection on the external S/4HANA service ───────────────────────────────
7// Expose only the fields needed — CAP delegates reads to S/4 transparently
8service SalesOrderService @(path: '/sales') {
9
10 @readonly
11 entity SalesOrders as projection on s4.A_SalesOrder {
12 key SalesOrder,
13 SalesOrderType,
14 SoldToParty,
15 RequestedDeliveryDate,
16 TotalNetAmount,
17 TransactionCurrency,
18 }
19
20 // Enriched view: local annotations merged with remote S/4 data
21 entity SalesOrderItems as projection on s4.A_SalesOrderItem {
22 key SalesOrder,
23 key SalesOrderItem,
24 Material,
25 OrderQuantity,
26 RequestedQuantityUnit,
27 NetAmount,
28 }
29}1// srv/s4-handler.js — transparent delegation + enrichment pattern
2const cds = require('@sap/cds')
3
4module.exports = cds.service.impl(async function () {
5
6 // Connect to S/4HANA via the named BTP Destination 'S4H_SALES'
7 // The Destination must point to the S/4HANA OData service URL
8 const S4 = await cds.connect.to('API_SALES_ORDER_SRV')
9
10 // ─── Pattern 1: Transparent Delegation ────────────────────────────────────
11 // Every READ on SalesOrders is forwarded verbatim to S/4HANA
12 this.on('READ', 'SalesOrders', async req => {
13 // req.query contains the OData $filter/$expand/$select — passed as-is
14 return S4.run(req.query)
15 })
16
17 // ─── Pattern 2: Mashup — local + remote data merged ───────────────────────
18 this.on('READ', 'SalesOrders', async req => {
19 // Fetch from S/4HANA
20 const orders = await S4.run(req.query)
21
22 // Fetch local annotations (e.g. CRM notes stored in HANA Cloud)
23 const orderIds = orders.map(o => o.SalesOrder)
24 const localNotes = await SELECT.from('LocalOrderNotes')
25 .where({ salesOrderId: { in: orderIds } })
26
27 // Merge
28 const notesMap = Object.fromEntries(localNotes.map(n => [n.salesOrderId, n.note]))
29 return orders.map(order => ({
30 ...order,
31 localNote: notesMap[order.SalesOrder] ?? null,
32 }))
33 })
34
35 // ─── Pattern 3: CAP Outbound — write back to S/4HANA ─────────────────────
36 this.on('CREATE', 'SalesOrders', async req => {
37 // Validate locally, then create in S/4HANA
38 const { SoldToParty, TotalNetAmount } = req.data
39 if (!SoldToParty) return req.reject(400, 'SoldToParty is required')
40
41 // Forward CREATE to S/4HANA OData service
42 const result = await S4.run(
43 INSERT.into('A_SalesOrder').entries(req.data)
44 )
45 return result
46 })
47
48})SAP HANA Cloud and Persistence
CAP uses SAP HANA Cloud HDI (HANA Deployment Infrastructure) containers for production persistence on BTP. HDI manages schema versioning, delta deployment, and container lifecycle. Each application (or tenant in SaaS deployments) gets its own HDI container — database artefacts are version-controlled alongside application code.
Local Development (SQLite)
- cds watch launches SQLite in-memory DB automatically
- All CDS DDL is applied on startup — no migration needed
- Test data from db/data/*.csv files auto-loaded
- Schema changes reflected immediately on file save
- Zero-config — no Docker or external DB required
Production (HANA Cloud HDI)
- HDI container bound as CF service in mta.yaml
- cds build generates .hdbcds and .hdbtable artefacts
- HDI deployer applies delta schema migrations on deploy
- Tenant isolation via separate HDI containers (MTX)
- HANA views, procedures, and functions in CDS or raw HDBSQL
// db/data/com.acme.bookshop-Books.csv // One row per entity — auto-loaded in dev and tests ID,title,stock,price 8a66e...d1,Wuthering Heights,10,12.99 9b77f...e2,Jane Eyre,5,9.99
// srv/catalog-service.cds — expose a HANA function
function stockSummary() returns {
totalBooks : Integer;
totalStock : Integer;
avgPrice : Decimal(9,2);
}
// Implemented as a HANA calculation view
// or DB function in db/src/Deployment — MTA and CI/CD
CAP applications are packaged as an MTA (Multi-Target Application) archive (.mtar) and deployed to BTP Cloud Foundry using cf deploy (via the MTA CF CLI plugin). The mta.yaml descriptor declares all modules (application server, HDI deployer, optional MTX sidecar) and BTP service resources (HANA, XSUAA, Destination). For Kyma deployments, a Helm chart is generated from the MTA descriptor.
1# mta.yaml — BTP Cloud Foundry deployment descriptor for CAP Node.js application
2_schema-version: '3.1'
3ID: bookshop-cap
4version: 1.0.0
5description: CAP Bookshop — BTP Cloud Foundry Deployment
6parameters:
7 enable-parallel-deployments: true
8
9modules:
10
11 # ── CAP Application Server ────────────────────────────────────────────────────
12 - name: bookshop-srv
13 type: nodejs
14 path: gen/srv
15 parameters:
16 memory: 512M
17 disk-quota: 1024M
18 buildpack: nodejs_buildpack
19 build-parameters:
20 builder: npm
21 build-result: .
22 requires:
23 - name: bookshop-db
24 - name: bookshop-auth
25 - name: bookshop-destination
26 - name: bookshop-connectivity
27 - name: bookshop-logging
28 provides:
29 - name: srv-api
30 properties:
31 srv-url: ${default-url}
32
33 # ── HANA HDI Deployer ──────────────────────────────────────────────────────────
34 - name: bookshop-db-deployer
35 type: hdb
36 path: gen/db
37 parameters:
38 memory: 256M
39 disk-quota: 512M
40 requires:
41 - name: bookshop-db
42
43resources:
44
45 # ── SAP HANA Cloud HDI Container ───────────────────────────────────────────────
46 - name: bookshop-db
47 type: com.sap.xs.hdi-container
48 parameters:
49 service: hana
50 service-plan: hdi-shared
51 properties:
52 hdi-service-name: ${service-name}
53
54 # ── XSUAA Authorization and Trust Management ───────────────────────────────────
55 - name: bookshop-auth
56 type: org.cloudfoundry.managed-service
57 parameters:
58 service: xsuaa
59 service-plan: application
60 path: ./xs-security.json
61 config:
62 xsappname: bookshop-cap
63 tenant-mode: dedicated
64
65 # ── Destination Service (for S/4HANA connectivity) ─────────────────────────────
66 - name: bookshop-destination
67 type: org.cloudfoundry.managed-service
68 parameters:
69 service: destination
70 service-plan: lite
71
72 # ── Connectivity Service (for Cloud Connector tunnels) ─────────────────────────
73 - name: bookshop-connectivity
74 type: org.cloudfoundry.managed-service
75 parameters:
76 service: connectivity
77 service-plan: lite
78
79 # ── Application Logging (SAP Cloud Logging) ────────────────────────────────────
80 - name: bookshop-logging
81 type: org.cloudfoundry.managed-service
82 parameters:
83 service: application-logs
84 service-plan: lite1# .github/workflows/deploy-btp.yml — GitHub Actions CI/CD for CAP on BTP
2name: CAP Build and Deploy to BTP Cloud Foundry
3
4on:
5 push:
6 branches: [main]
7 pull_request:
8 branches: [main]
9
10env:
11 NODE_VERSION: '20'
12
13jobs:
14 test:
15 name: Unit Tests and Type Check
16 runs-on: ubuntu-latest
17 steps:
18 - uses: actions/checkout@v4
19
20 - name: Set up Node.js ${{ env.NODE_VERSION }}
21 uses: actions/setup-node@v4
22 with:
23 node-version: ${{ env.NODE_VERSION }}
24 cache: npm
25
26 - name: Install dependencies
27 run: npm ci
28
29 - name: Generate CDS TypeScript types
30 run: npx cds-typer '*' --outputDirectory @cds-models
31
32 - name: Run unit tests
33 run: npm test
34
35 build-and-deploy:
36 name: Build MTAR and Deploy
37 runs-on: ubuntu-latest
38 needs: test
39 if: github.ref == 'refs/heads/main'
40 steps:
41 - uses: actions/checkout@v4
42
43 - name: Set up Node.js ${{ env.NODE_VERSION }}
44 uses: actions/setup-node@v4
45 with:
46 node-version: ${{ env.NODE_VERSION }}
47 cache: npm
48
49 - name: Install dependencies
50 run: npm ci
51
52 - name: Build CDS artefacts
53 run: npx cds build --production
54
55 - name: Install MBT build tool
56 run: npm install -g mbt
57
58 - name: Build MTAR archive
59 run: mbt build --platform cf --mtar bookshop.mtar
60
61 - name: Install CF CLI
62 run: |
63 curl -L "https://packages.cloudfoundry.org/stable?release=linux64-binary&version=v8" | tar -zx
64 sudo mv cf8 /usr/local/bin/cf
65
66 - name: Deploy to BTP Cloud Foundry (blue-green)
67 run: |
68 cf api ${{ secrets.CF_API_ENDPOINT }}
69 cf auth ${{ secrets.CF_USERNAME }} ${{ secrets.CF_PASSWORD }}
70 cf target -o ${{ secrets.CF_ORG }} -s ${{ secrets.CF_SPACE }}
71 cf deploy mta_archives/bookshop.mtar --strategy blue-green --no-confirmCAP Extension Patterns
CAP supports both side-by-side extensions (standalone CAP application on BTP, consuming S/4HANA APIs) and embedded extensions (CAP deployed inside S/4HANA ABAP Cloud Environment). Additional patterns include the plugin architecture for reusable cross-cutting concerns.
Standalone CAP app on BTP Cloud Foundry or Kyma. Consumes S/4HANA released OData V4 APIs via BTP Destination Service. No modifications to S/4HANA system. Clean Core compliant.
Use when: New UI and logic on top of S/4HANA data
CAP application consumes multiple external services (S/4HANA, Ariba, SuccessFactors) via cds.connect.to(). Mashup and federation patterns. All connections via BTP Destination Service.
Use when: Multi-system data aggregation and federation
Reusable CAP plugins for attachments (@cap-js/attachments), audit logging (@cap-js/audit-logging), change tracking (@cap-js/change-tracking), and telemetry (@cap-js/telemetry).
Use when: Cross-cutting concerns without custom handler code
CDS @UI and @Fiori annotations drive SAP Fiori Elements UI generation. No front-end code for standard list, object page, and analytical list page floorplans. Generated EDMX drives the UI.
Use when: SAP standard floorplan apps without custom UI code
Tenant-specific CDS model extensions — subscribers can add custom fields and entities via the MTX Extension API. New fields are reflected in OData and the HANA schema without redeployment.
Use when: SaaS products where tenants need custom fields
CAP emits CloudEvents to SAP Event Mesh or local message queues. Handlers registered via srv.on("OrderConfirmed", ...) for async workflows. Integrates with SAP Integration Suite event publishers.
Use when: Decoupled microservice communication and workflows
Licensing and Commercial Model
The CAP framework itself is free and open-source (Apache 2.0). There is no licensing cost for the @sap/cds or @sap/cds-dk packages. BTP infrastructure costs (Cloud Foundry memory, HANA Cloud capacity, XSUAA, Destination Service) apply when deploying on SAP BTP.
CAP
An open-source, convention-over-configuration framework for building cloud-native services and applications on SAP BTP using CDS, Node.js, and Java.
Framework is free (Apache 2.0). BTP infrastructure costs apply when deployed on Cloud Foundry, Kyma, or ABAP environment.
CAP Runtime — BTP Service Dependencies and Commercial Models
BTP Service | Required for CAP?Always needed | Commercial ModelCPEA / Free | NotesKey cost driver |
|---|---|---|---|
| Mandatory Services | |||
| CAP Framework (@sap/cds) | Free (Apache 2.0) | No cost — open source | |
| Cloud Foundry Runtime | CPEA / BTPEA | Memory-based consumption; free tier available | |
| SAP HANA Cloud | Capacity units | Primary cost driver — included in RISE | |
| XSUAA (Auth & Trust Mgmt) | Free | Included with CF runtime at no charge | |
| Common Optional Services | |||
| SAP Destination Service | Free (lite plan) | Required for S/4HANA connectivity | |
| SAP Connectivity Service | Free (lite plan) | Required for Cloud Connector (on-premise) | |
| SAP Event Mesh | CPEA (messages) | Required for async event publishing | |
| Audit Log Service | CPEA | @cap-js/audit-logging plugin | |
| Application Logging | CPEA (lite plan) | SAP Cloud Logging — structured log shipping | |
SAP CAP Road Map — Current and Upcoming Features
cds-typer — TypeScript Type Generation
Generally AvailableGenerates TypeScript types for all CDS entities, services, and events. Run npx cds-typer to produce typed stubs in @cds-models/. Eliminates runtime string-based entity references.
@cap-js Plugin Ecosystem (attachments, audit-log, change-tracking, telemetry)
Generally AvailableFour official @cap-js plugins for common cross-cutting concerns. @cap-js/attachments handles binary large objects via Object Store. @cap-js/telemetry exports OpenTelemetry traces and metrics to SAP Cloud Logging or Dynatrace.
CAP Native GraphQL Adapter
Generally AvailableProvides a GraphQL endpoint for any CAP service — automatically derived from CDS service definitions. Exposes the same entities and authorisation rules as the OData V4 endpoint. npm install @cap-js/graphql.
Hybrid Testing (cds bind)
Generally Availablecds bind connects local CAP services to real BTP service instances (HANA Cloud, XSUAA) without a full deployment. Enables realistic integration testing from a developer laptop.
CAP AI SDK Integration
RoadmapNative CAP integration with SAP AI SDK for embedding GenAI capabilities directly in CAP handlers. Enables direct calls to Generative AI Hub LLMs from within CDS event handlers. No GA date confirmed as of June 2025.
CAP on ABAP Cloud (Embedded)
RoadmapRunning CAP Node.js within the ABAP Cloud (BTP ABAP Environment) runtime — enabling CAP logic close to SAP core systems without an additional CF or Kyma runtime. Direction confirmed; no GA date as of June 2025.
Best Practices
Use aspects — never repeat field definitions
Apply managed and cuid to every entity that needs audit fields and UUID keys. Create project-level aspects (e.g. SoftDelete, TenantAware) for cross-cutting concerns.
Keep service definitions thin
Services should project entities — not re-define them. All data modelling belongs in db/schema.cds. Services add annotations, restrictions, and projections only.
Use before handlers for all input validation
Never validate in on handlers — on handlers run business logic. Centralise all req.reject() calls in before handlers to separate concerns cleanly.
Never hard-code remote service URLs
All external services (S/4HANA, Ariba, SuccessFactors) must be bound via cds.connect.to() referencing a BTP Destination name — never a hard-coded URL.
Enable cds-typer for TypeScript-safe handlers
Run npx cds-typer in your build pipeline to generate typed stubs. Use typed entity references instead of string literals to catch schema drift at compile time.
Use mocked services in unit tests
Set cds.env.requires.<svc>.kind = "mock" in cds.test() to avoid real S/4HANA connections in tests. Test business logic isolation — not connectivity.
Separate read and write services
Expose a @readonly service for Fiori Elements browsing and a separate admin service with @restrict for writes. Simpler authorisation, fewer accidental mutations.
Always build with --production before deploy
cds build --production generates optimised CSN and removes development-only features. Never deploy the source directly — always deploy from gen/srv and gen/db.
Common Pitfalls
Defining business logic in CDS service files
Fix: CDS services are declarations only — no logic. All imperative logic belongs in the corresponding .js or .java handler file. CDS annotations handle declarative constraints.
Using req.query directly to build DB statements
Fix: Never use req.query.SELECT.where directly in custom DB queries. Use the structured CQL API: SELECT.from(Entity).where({...}) — this prevents injection and ensures scope isolation.
Missing @assert annotations — validating only in handlers
Fix: Use CDS built-in @mandatory, @assert.range, and @assert.format annotations for declarative validation. Handler validation is then reserved for business rules, not field-level checks.
Binding the same XSUAA instance to multiple applications
Fix: Each CAP application must have its own XSUAA service instance with its own xs-security.json. Sharing XSUAA instances between applications mixes role collections and scope namespaces.
Deploying with cds deploy instead of MTA
Fix: cds deploy is for local dev only — it uses default service bindings and cannot manage BTP services. Production deployments must use mta.yaml + cf deploy to provision and bind BTP services correctly.
Ignoring N+1 query problems in after handlers
Fix: Avoid per-row DB calls in after READ handlers. Use a single bulk SELECT with IN clause to enrich all rows at once, or use $expand in the OData query to let CAP handle the join efficiently.
SAP Official References
CAP Documentation — cap.cloud.sap
Complete official CAP documentation — CDS, Node.js, Java, deployment, multitenancy, and plugins.
CAP on GitHub (open source)
SAP CAP open-source organisation — @cap-js plugins, samples, and issue tracking.
SAP BTP CAP — Discovery Center
Official CAP learning mission on SAP Discovery Center — step-by-step guided development.
CDS Language Reference
Complete reference for CDS Definition Language (CDL) — entities, services, aspects, annotations.
CAP Node.js API Reference
Node.js runtime API — cds.service, cds.connect.to, CQL SELECT/INSERT/UPDATE/DELETE.
CAP Java Reference
Java runtime documentation — Spring Boot integration, CqnService, event handlers, HANA deployment.
SAP CAP Road Map Explorer
Official SAP Road Map for CAP — GA, Planned, and Roadmap features.
CAP MTA Deployment Guide
Step-by-step deployment guide for MTA, CF, Kyma, and CI/CD pipeline setup.