| name | remote-services |
| description | Use when integrating SAP CAP applications with remote services: external OData, S/4HANA integration, cds.connect.to, cds import edmx, BTP Connectivity, Destination Service, mashup, req.query delegation, REST service, remote service proxy, service binding, hybrid testing.
|
| metadata | {"version":"1.1.0","keywords":["remote service","cds.connect.to","cds import","S/4HANA","OData","external API","mashup","delegation","BTP Connectivity","destination"],"related":{"btp-destinations":"configure BTP destinations for remote services","btp-service-bindings":"bind Connectivity service locally","performance":"optimize remote service calls"}} |
Remote Services — CAP Best Practices
Primary reference: https://cap.cloud.sap/docs/guides/using-services
Consuming services: https://cap.cloud.sap/docs/node.js/remote-services
Import the external service definition
cds import ./BusinessPartner_A2X.edmx --as cds
cds import https://my.s4system.example.com/sap/opu/odata/sap/API_BUSINESS_PARTNER/$metadata \
--as cds --force
This generates srv/external/API_BUSINESS_PARTNER.cds — commit this file!
Configure the service binding
package.json:
{
"cds": {
"requires": {
"API_BUSINESS_PARTNER": {
"kind": "odata-v2",
"model": "srv/external/API_BUSINESS_PARTNER",
"[production]": {
"credentials": {
"destination": "S4HANA_PROD",
"path": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
}
}
}
}
}
}
For local dev, use default-env.json (never commit!) with real or sandbox credentials.
Connect and query
module.exports = class BusinessPartnerService extends cds.ApplicationService {
async init() {
this.S4 = await cds.connect.to('API_BUSINESS_PARTNER')
this.on('READ', BusinessPartners, this.onReadBusinessPartners)
return super.init()
}
async onReadBusinessPartners(req) {
return this.S4.run(req.query)
}
}
Mashup: extend remote data with local data
// Extend the remote entity with local fields
extend projection BusinessPartners with {
internalScore : Integer // stored locally
}
async onReadBusinessPartners(req) {
const bps = await this.S4.run(req.query)
const ids = bps.map(bp => bp.BusinessPartner)
const scores = await SELECT.from(LocalScores).where({ bp_id: { in: ids } })
const scoreMap = Object.fromEntries(scores.map(s => [s.bp_id, s.score]))
for (const bp of bps) bp.internalScore = scoreMap[bp.BusinessPartner] ?? null
return bps
}
Forwarding $filter, $top, $skip
When you delegate req.query directly, CAP translates OData query options automatically. But if you need custom filtering:
const query = SELECT.from('A_BusinessPartner')
.where({ BusinessPartnerCategory: '2' })
.columns('BusinessPartner', 'BusinessPartnerFullName', 'SearchTerm1')
.limit(req.query.SELECT.limit ?? 50)
return this.S4.run(query)
Destination Service (BTP Connectivity)
For CF/Kyma deployments, configure the Destination in BTP Cockpit:
| Field | Value |
|---|
| Name | S4HANA_PROD |
| Type | HTTP |
| URL | https://my.s4.example.com |
| Auth | BasicAuthentication or OAuth2SAMLBearerAssertion |
| Additional Properties | WebIDEEnabled = true |
Bind the destination service in mta.yaml:
- name: my-app-srv
requires:
- name: my-app-destination
- name: my-app-connectivity
- name: my-app-connectivity
type: org.cloudfoundry.managed-service
parameters:
service: connectivity
service-plan: lite
REST services
{
"cds": {
"requires": {
"MyRestAPI": {
"kind": "rest",
"credentials": {
"destination": "MY_REST_DESTINATION"
}
}
}
}
}
const api = await cds.connect.to('MyRestAPI')
const result = await api.get('/v1/resource', { params: { id: 123 } })
const created = await api.post('/v1/resource', { name: 'New Item' })
Local mock for development
{
"cds": {
"requires": {
"API_BUSINESS_PARTNER": {
"kind": "odata-v2",
"model": "srv/external/API_BUSINESS_PARTNER",
"[development]": {
"kind": "odata",
"credentials": {
"url": "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/API_BUSINESS_PARTNER",
"headers": { "APIKey": "{{SAP_API_HUB_KEY}}" }
}
}
}
}
}
}
Common mistakes to avoid
- ❌ Not committing the imported
.cds file — teammates can't compile without it
- ❌ Fetching all fields from remote then filtering locally — always push filters to remote
- ❌ Opening a new connection per request (
cds.connect.to in handler body) — connect in init()
- ❌ Hardcoding API keys in
package.json — use environment variables or destinations
- ❌ Forgetting to add
connectivity service for on-premise systems (Cloud Connector)
- ❌ Not handling remote service errors (see
error-handling skill)
Publishing your service as an API package (cds export GA, CDS 10+)
Instead of exporting to OData EDMX and having consumers cds import it, use cds export to create a ready-to-use CAP API client package with lossless CDS models:
cds export srv/data-service.cds
npm publish ./apis/data-service
npm add @your-org/data-service
This allows consumers to import your service definitions natively instead of via EDMX conversion.