Open Source
Node.js
Java
OData V4
Clean Core
HANA Cloud

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

CAP Node.js — Build a Service in 20 Min21:05

CAP Node.js — Build a Service in 20 Min

SAP Developers
Beginner
CDS Data Modelling Deep Dive35:22

CDS Data Modelling Deep Dive

SAP Developers
Intermediate
CAP Multitenancy with MTX Extension28:47

CAP Multitenancy with MTX Extension

SAP Developers
Advanced
CAP Java — Getting Started19:30

CAP Java — Getting Started

SAP Learning
Beginner
DevelopmentSAP CAP
SAP Docs
SAP CAP is the recommended programming model for building cloud-native business services on SAP BTP. Built on Core Data Services (CDS) for data and service modelling, with Node.js and Java runtimes. Exposes OData V4 and REST endpoints consumed by SAP Fiori Elements and external applications. Bound to SAP BTP services (XSUAA, HANA Cloud, Destination) at deploy time.
Rendering diagram…

SAP CAP — Architectural Layers

Consumers
SAP Fiori Elements
Auto-Generated UI from CDS
External Applications
Mobile, Third-Party
Protocol Adapters
OData V4 Adapter
Fiori and RAP Consumption
REST Adapter
Microservice APIs
Event Handler Layer
Before Hooks
Input Validation
On Handlers
Business Logic
After Hooks
Post-Processing
CDS Layer — Core Data Services
Data Model
.cds schema files
Service Definition
.cds service files
Persistence Layer
SAP HANA Cloud
Production
SQLite
Local Development

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 Build Pipeline — From Source to HANA DDL and EDMX
Rendering diagram…

CDS Entity Definitions — Schema with Aspects

db/schema.cds
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 aspect

Adds a generated UUID primary key. Applied as `: cuid` mixin — no manual key field needed.

managed aspect

Adds createdAt, createdBy, modifiedAt, modifiedBy fields. Auto-populated by CAP from the JWT context.

localized

Marks a String field for automatic localisation. CAP generates a companion _texts table for translated values.

Composition

Declares a parent-child ownership relationship. Deep insertions and cascading deletes are handled by CAP.

Association

Declares a navigable relationship (foreign key). No automatic cascade — applications control the lifecycle.

@assert.range

Built-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

srv/catalog-service.cds
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}
@restrict vs @requires
Use @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.

CAP Request Lifecycle — Authentication, Authorisation, and Handler Pipeline
Rendering diagram…
srv/catalog-handler.js
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/Books

Collection read — supports $filter, $select, $expand, $orderby, $top, $skip

GET /catalog/Books(ID)

Single entity read by key — $expand for associations

POST /catalog/Books

Deep insert — nested OrderItems composition inserted atomically

POST /catalog/submitOrder

Bound 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/$metadata

OData V4 EDMX metadata document — consumed by Fiori Elements

POST /catalog/$batch

OData V4 batch — multiple requests in one HTTP call, atomic or not

GET /catalog/Books?$filter=stock gt 0

Server-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.

xs-security.json
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 in handlers
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
Programmatic auth check
// 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.

CAP Multitenancy — MTX Sidecar, HDI Container Isolation per Tenant
Rendering diagram…
srv/mtx-handler.js
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})
Enable multitenancy
// package.json — add mtxs
{
  "cds": {
    "requires": {
      "multitenancy": true
    }
  }
}
MTX sidecar app router
# mta.yaml — add MTX module
- name: bookshop-mtx
  type: nodejs
  path: mtx/sidecar
  requires:
    - name: bookshop-db
    - name: bookshop-auth
Tenant-specific query
// 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.

PatternDescriptionUse when
Transparent DelegationCAP passes req.query directly to the remote service — response returned as-isSimple proxy — no local data involved
Mashup / EnrichmentMerge remote S/4 data with local HANA data in a single responseAdding local metadata (notes, flags) to S/4 entities
Cached ReplicationCAP replicates remote data into local HANA via events — reads always localHigh-read, low-write remote entities (e.g. cost centres)
Outbound WriteCAP forwards CREATE/UPDATE to S/4HANA via the external service connectionWrite-back to S/4HANA from BTP extension
Mocked External Servicecds.env.requires.<svc>.kind = "odata-v4" replaced by "mock" in testsUnit tests without live S/4HANA connection
srv/s4-integration.cds
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}
srv/s4-handler.js
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
CSV seed data
// 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
HANA native function
// 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.

CAP Deployment Pipeline — From cds watch to BTP Blue-Green Deploy
Rendering diagram…
mta.yaml
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: lite
.github/workflows/deploy-btp.yml
1# .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-confirm

CAP 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.

Side-by-Side ExtensionRecommended

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 Remote ServicesIntegration

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

@cap-js PluginsGA

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

CAP with Fiori ElementsGA

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

Extensibility via mtxsSaaS

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

Event-Driven IntegrationGA

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.

Status:Generally AvailablePlannedRoadmapFuture Direction
Development

CAP

Generally Available· GA — continuously updated; see cap.cloud.sap for releases

An open-source, convention-over-configuration framework for building cloud-native services and applications on SAP BTP using CDS, Node.js, and Java.

Free

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 RuntimeCPEA / BTPEAMemory-based consumption; free tier available
SAP HANA CloudCapacity unitsPrimary cost driver — included in RISE
XSUAA (Auth & Trust Mgmt)FreeIncluded with CF runtime at no charge
Common Optional Services
SAP Destination ServiceFree (lite plan)Required for S/4HANA connectivity
SAP Connectivity ServiceFree (lite plan)Required for Cloud Connector (on-premise)
SAP Event MeshCPEA (messages)Required for async event publishing
Audit Log ServiceCPEA@cap-js/audit-logging plugin
Application LoggingCPEA (lite plan)SAP Cloud Logging — structured log shipping

SAP CAP Road Map — Current and Upcoming Features

Availability distinctions apply
Items below are labelled by their SAP availability status as of June 2025. Roadmap and Future Direction items are not currently available — do not plan production implementations against uncommitted items. Source: SAP Road Map Explorer, cap.cloud.sap, and SAP Sapphire 2025 announcements.

cds-typer — TypeScript Type Generation

Generally Available
GA — 2024

Generates 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 Available
GA — 2024

Four 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 Available
GA — @cap-js/graphql plugin

Provides 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 Available
GA

cds 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

Roadmap
Roadmap — direction confirmed, date TBD

Native 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)

Roadmap
Roadmap — direction confirmed

Running 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.