Import S/4HANA OData metadata, connect via Destination Service, and implement the delegate pattern.
S/4HANA is the core ERP for most enterprises on BTP. CAP apps routinely extend S/4HANA — reading master data, creating documents, subscribing to business events. This is the most common real-world integration pattern in the SAP ecosystem.
# Download from SAP API Business Hub (API Hub)
# https://api.sap.com → search "Business Partner" → Download API Specification → EDMX
cds import ./API_BUSINESS_PARTNER.edmx --as cds
# Creates srv/external/API_BUSINESS_PARTNER.cds
// srv/procurement-service.cds
using { API_BUSINESS_PARTNER as bp } from './external/API_BUSINESS_PARTNER';
service ProcurementService {
entity Suppliers as projection on bp.A_BusinessPartner {
key BusinessPartner as ID,
BusinessPartnerFullName as name,
Country as country,
to_BusinessPartnerAddress as addresses
}
}
Name: S4HANA_CLOUD
Type: HTTP
URL: https://<tenant>.s4hana.ondemand.com
Authentication: OAuth2ClientCredentials
Token Service URL: https://<tenant>.authentication.eu20.hana.ondemand.com/oauth/token
Client ID: sb-<service-instance>!<id>
Client Secret: <communication arrangement secret>
Create a Communication Arrangement in S/4HANA for the specific API (e.g., SAP_COM_0008 for Business Partner API), then copy the credentials to the destination.
// .cdsrc.json
{
"requires": {
"API_BUSINESS_PARTNER": {
"kind": "odata-v2",
"model": "srv/external/API_BUSINESS_PARTNER",
"[production]": {
"destination": "S4HANA_CLOUD",
"path": "/sap/opu/odata/sap"
},
"[development]": {
"credentials": {
"url": "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap",
"headers": { "APIKey": "YOUR_DEV_KEY" }
}
}
}
}
}
// srv/procurement-service.js
const s4 = await cds.connect.to('API_BUSINESS_PARTNER')
// Delegate all READs on Suppliers to S/4HANA
this.on('READ', 'Suppliers', (req) => s4.run(req.query))
CAP translates $filter, $select, $expand, $top, $skip and sends them to S/4HANA.
this.on('READ', 'PurchaseOrders', async (req) => {
// 1. Read local POs
const orders = await SELECT.from('PurchaseOrders')
// 2. Deduplicate supplier IDs
const supplierIds = [...new Set(orders.map(o => o.supplier_ID))]
// 3. Fetch supplier names from S/4HANA in one call
const suppliers = await s4.run(
SELECT.from('A_BusinessPartner')
.where({ BusinessPartner: { in: supplierIds } })
.columns('BusinessPartner', 'BusinessPartnerFullName')
)
// 4. Merge
const map = Object.fromEntries(suppliers.map(s => [s.BusinessPartner, s.BusinessPartnerFullName]))
return orders.map(o => ({ ...o, supplierName: map[o.supplier_ID] ?? 'Unknown' }))
})
| Quirk | Workaround |
|---|---|
Dates as /Date(timestamp)/ |
CAP converts automatically with kind: odata-v2 |
| CSRF token required for write | CAP fetches and sends X-CSRF-Token automatically |
$batch uses V2 format |
CAP handles automatically |
Error in d.error vs error |
CAP normalises to standard OData error format |
Association naming (_to_) |
Use the generated CDS model — it maps names correctly |
this.on('createPurchaseOrder', async (req) => {
try {
const result = await s4.run(
INSERT.into('A_PurchaseOrder').entries(req.data)
)
return result
} catch (err) {
// S/4HANA errors come back as structured OData errors
if (err.statusCode === 400) {
return req.reject(400, `S/4HANA rejected the PO: ${err.message}`)
}
if (err.statusCode === 403) {
return req.reject(403, 'Insufficient authorizations in S/4HANA')
}
throw err // unexpected — rethrow for CAP global error handler
}
})
GET /procurement/Suppliers?$top=5cds import ./file.edmx --as cds generates a CDS model from EDMXkind: odata-v2 handles OData V2 quirks (dates, CSRF) automaticallys4.run(req.query) — full OData query forwarding