Import OData metadata with cds import, connect to external services with cds.connect.to(), and use the delegate pattern.
CAP apps rarely live alone — they call S/4HANA OData APIs, BTP services, and partner APIs. CAP's cds.connect.to() pattern abstracts the transport so your handler code is the same whether the remote service is a real S/4HANA system or a local mock.
# Download the API definition from SAP API Business Hub (edmx format)
# Or use CDS introspection
cds import ./API_BUSINESS_PARTNER.edmx --as cds
This creates:
srv/external/
└── API_BUSINESS_PARTNER.cds ← CDS model of the external API
// Auto-generated (excerpt)
service API_BUSINESS_PARTNER {
entity A_BusinessPartner {
key BusinessPartner : String(10);
BusinessPartnerFullName : String;
// ...
}
}
// srv/orders-service.cds
using { API_BUSINESS_PARTNER as bp } from './external/API_BUSINESS_PARTNER';
service OrdersService {
entity Orders as projection on my.Orders;
// Expose a subset of the remote entity
entity Suppliers as projection on bp.A_BusinessPartner {
key BusinessPartner as ID,
BusinessPartnerFullName as name
}
}
// .cdsrc.json
{
"requires": {
"API_BUSINESS_PARTNER": {
"[production]": {
"kind": "odata-v2",
"model": "srv/external/API_BUSINESS_PARTNER",
"destination": "S4HANA_DESTINATION", // BTP Destination name
"path": "/sap/opu/odata/sap"
},
"[development]": {
"kind": "odata-v2",
"model": "srv/external/API_BUSINESS_PARTNER",
"credentials": {
"url": "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap",
"headers": { "APIKey": "YOUR_API_KEY" }
}
}
}
}
}
// srv/orders-service.js
module.exports = class OrdersService extends cds.ApplicationService {
async init () {
const bpApi = await cds.connect.to('API_BUSINESS_PARTNER')
// Delegate all READ on Suppliers to the remote S/4HANA API
this.on('READ', 'Suppliers', (req) => bpApi.run(req.query))
await super.init()
}
}
The req.query is the CQL query built from the OData request — CAP translates $filter, $select, $top, etc. and delegates them to the remote API automatically.
this.on('READ', 'EnrichedOrders', async (req) => {
// 1. Read local orders
const orders = await SELECT.from('Orders')
// 2. Get unique supplier IDs
const supplierIds = [...new Set(orders.map(o => o.supplier_ID))]
// 3. Fetch supplier names from S/4HANA
const suppliers = await bpApi.run(
SELECT.from('A_BusinessPartner')
.where({ BusinessPartner: { in: supplierIds } })
.columns('BusinessPartner', 'BusinessPartnerFullName')
)
// 4. Merge
const supplierMap = Object.fromEntries(suppliers.map(s => [s.BusinessPartner, s.BusinessPartnerFullName]))
return orders.map(o => ({ ...o, supplierName: supplierMap[o.supplier_ID] || 'Unknown' }))
})
Create a CSV file to mock the remote entity locally:
srv/external/data/
└── API_BUSINESS_PARTNER-A_BusinessPartner.csv
BusinessPartner,BusinessPartnerFullName
1000000,ACME Corporation
1000001,GlobalTech Ltd
When kind is not set (no .cdsrc.json entry for dev), CAP auto-mocks using these CSV files.
try {
const suppliers = await bpApi.run(SELECT.from('A_BusinessPartner').where({ BusinessPartner: id }))
return suppliers
} catch (err) {
if (err.statusCode === 404) return req.reject(404, `Supplier ${id} not found in S/4HANA`)
console.error('S/4HANA call failed:', err.message)
return req.reject(502, 'Remote service temporarily unavailable')
}
| Mistake | Symptom | Fix |
|---|---|---|
Passing req not req.query |
Delegation error | Use bpApi.run(req.query) |
| No mock CSV for dev | Empty results in local testing | Add CSV under srv/external/data/ |
Missing using import |
CDS model not found | Add using { ... } from './external/...' |
| Destination not configured in BTP | 404 in production | Create the destination in BTP Cockpit |
Suppliers as a projection in your servicecds import generates CDS from OData/edmxcds.connect.to('ServiceName') returns a service clientbpApi.run(req.query) delegates the full query including filterssrv/external/data/ enable local development without a real backend