CAP-Level Service Integration
The 'Calesi' Pattern
Integrating remote services - from other applications, third-party services, or platform services - is a fundamental aspect of cloud application development. CAP provides an easy and platform-agnostic way to do so: Remote services represented as CAP services, which you can consume as if they were local, while the CAP runtimes manage the communication and resilience details under the hood. Not the least, CAP mocks remote services automatically for local inner-loop development and testing.
The 'Calesi' Pattern – Guiding Principles
- Remote services are proxied by CAP services, ... → everything's a CAP service
- Consumed in protocol-agnostic ways → ... as if they were local
- Mocked out of the box → fast-track inner-loop development
- With varying implementations → evolution w/o disruption
- Extensible through event handlers → intrinsic extensibility
=> Application developers stay at CAP level -> Focused on Domain
Preliminaries
Teaser
With CAP, Service integration is greatly simplified. Consumption of remote services from other applications, third-party services, or platform services is as easy as calling them as if they were local:
Clone the bookshop sample, and start the server in a terminal:
shellgit clone https://github.com/capire/bookshop cds watch bookshopStart cds repl in a second terminal and run this code:
shellcds repljscats = await cds.connect.to ('http://localhost:4004/hcql/admin') await cats.read `Authors { ID, name, books { ID, title, genre.name as genre } }`1
2
3
4
5
6
Requires Cloud SDK libs ...
In case you get respective error messages ensure you've installed the following Cloud SDK packages in your project:
npm add @sap-cloud-sdk/connectivity
npm add @sap-cloud-sdk/http-client
npm add @sap-cloud-sdk/resilienceThe graphic below illustrates what happened here:
Remote CAP services can be consumed using the same high-level, uniform APIs as for local services – i.e., as if they were local. cds.connect automatically constructs remote proxies, which translate all local requests into protocol-specific ones, sent to remote services. Thereby also taking care of all connectivity, remote communication, principal propagation, as well as generic resilience.
Model Free
Note that in the exercise above, the consumer side didn't even have any information about the service provider, except for the URL endpoint and protocols served, which it got from the service binding. In particular no API/service definitions at all – neither in CDS, OData, nor in OpenAPI.
Overview
While the above teaser nicely demonstrates the simplicity of CAP-level service integration, CAP can facilitate real-life integration scenarios even more, if we've captured APIs in CDS models. The remainder of this guide walks us through the steps to provide and share such APIs, import them to consuming apps as CDS models and use these in there as if they were local. The graphic below shows the flow of essential steps involved:
The XTravels Sample
In this guide we use the XTravels sample application as our running example. It's a modernized adaptation of the ABAP Flight reference sample, reimplemented using CAP and split into two microservices:
The @capire/xflights service provides flight-related master data, such as Flights, Airports, Airlines, and Supplements (like extra luggage, meals, etc.). It exposes this data via a CAP service API.
The @capire/xtravels application allows travel agents to plan travels on behalf of travellers, including bookings of flights. The application obtains Customer data from a SAP S/4HANA system, while it consumes Flights, Airports, and Airlines from @capire/xflights, as indicated by the green and blue areas in the screenshot below.

The resulting entity-relationship model:
From a service integration perspective, this sample mainly shows a data federation scenario, where the application consumes data from different upstream systems (XFlights and S/4HANA) – most frequently in a readonly fashion – to display it together with the application's local data.
Getting Started
So, let's dive into the details of CAP-level service integration, using the XTravels sample as our running example. Clone both repositories as follows to follow along:
mkdir -p cap/samples
cd cap/samples
git clone https://github.com/capire/xflights
git clone https://github.com/capire/xtravelsProviding CAP-level APIs
In case of CAP service providers, as for @capire/xflights, you define CAP services for all inbound interfaces, which includes (private) interfaces to your application's UIs, as well as public APIs to any other remote consumers.
Defining Service APIs
Open the cap/samples/xflights folder in Visual Studio Code, and have a look at the service definition in srv/data-service.cds in there:
using sap.capire.flights as x from '../db/schema';
@odata @hcql service sap.capire.flights.data {
@readonly entity Flights as projection on x.Flights {flights.*,*};
@readonly entity Airlines as projection on x.Airlines;
@readonly entity Airports as projection on x.Airports;
}2
3
4
5
6
This declares a CAP service named sap.capire.flights.data, served over OData and HCQL protocols, which exposes Flights, Airlines, and Airports as readonly projections on underlying domain model entities, with Flights as a denormalized view.
Using Denormalized Views
Let's have a closer look at the denormalized view for Flights, which basically flattens the association to FlightConnection. The projection {flights.*,*} shown in line 3 above, is a simplified version of the following actual definition found in srv/data-service.cds:
@readonly entity Flights as projection on x.Flights {flights.*,*};
@readonly entity Flights as projection on x.Flights {
*, // all fields from Flights
flight.{*} excluding {ID}, // all fields from FlightConnection
key flight.ID, // with flight ID preserved as key
key date, // with date preserved as key
} excluding { flight }; // which we flattened above4
5
6
7
8
9
This definition is more complicated because we need to preserve the primary keys elements flight.ID and date, as OData disallows entities without keys.
Use Case-Oriented Services
Denormalized views are a common way to tailor provided APIs to fit your use case. While normalization is required within XFlights to avoid redundancies, we flatten it here, to make life easier for external consumers.
=> See also: Use Case-Oriented Services in the getting started guide.
Exporting APIs
Use cds export to generate APIs for given service definitions. For example, run that within the cap/samples/xflights folder for the service definition we saw earlier, which would print some output as shown below:
cds export srv/data-service.cdsExporting APIs to apis/data-service ...
> apis/data-service/services.csn
> apis/data-service/index.cds
> apis/data-service/package.json
/done.By default, it outputs to an ./apis/<service> subfolder, where <service> is the .cds file's basename. Use the --to option to specify a different output folder.
Exported Service Definitions
The essential component of the generated output is the services.csn file, which contains a cleansed, interface-only version of your service definition. It includes the inferred element signatures of served entities but removes all projections to underlying entities and their dependencies.
To get an idea of the effect, run cds export in dry-run mode like this:
cds export srv/data-service.cds --dry Kept: 6
• sap.capire.flights.data
• sap.capire.flights.data.Flights
• sap.capire.flights.data.Airlines
• sap.capire.flights.data.Airports
• sap.capire.flights.data.Supplements
• sap.capire.flights.data.SupplementTypes
Skipped: 31
- sap.capire.flights.Flights
- sap.capire.flights.FlightConnections
- sap.capire.flights.Airlines
- sap.capire.flights.Airports
- sap.capire.flights.Supplements
- sap.capire.flights.SupplementTypes
- Language
- Currency
- Country
- Timezone
- sap.common
- sap.common.Locale
- sap.common.Languages
- sap.common.Countries
- sap.common.Currencies
- sap.common.Timezones
- sap.common.CodeList
- sap.common.TextsAspect
- sap.common.FlowHistory
- cuid
- managed
- temporal
- User
- sap.capire.flights.Supplements.texts
- sap.capire.flights.SupplementTypes.texts
- sap.common.Languages.texts
- sap.common.Countries.texts
- sap.common.Currencies.texts
- sap.common.Timezones.texts
- sap.capire.flights.data.Supplements.texts
- sap.capire.flights.data.SupplementTypes.texts
Total: 37Compare to original service definition...
We can also compare the above to the respective output for the complete provided service like that:
cds export srv/data-service.cds --dry > x.log
cds minify srv/data-service.cds --dry > m.log
code --diff *.logThis opens a diff view in VSCode, which would display these differences:
Kept: 26
Kept: 6
• sap.capire.flights.data
• sap.capire.flights.data.Flights
•• sap.capire.flights.data.Airlines
•• sap.capire.flights.data.Airports
• sap.capire.flights.data.Supplements
•• sap.capire.flights.data.SupplementTypes
•• sap.capire.flights.Flights
••• sap.capire.flights.FlightConnections
•••• sap.capire.flights.Airlines
••••• sap.common.Currencies
•••••• sap.common.Currencies.texts
••••••• sap.common.Locale
••••••• sap.common.TextsAspect
•••••• sap.common.CodeList
••••• Currency
••••• cuid
•••• sap.capire.flights.Airports
••••• sap.common.Countries
•••••• sap.common.Countries.texts
••••• Country
•• sap.capire.flights.Supplements
••• sap.capire.flights.SupplementTypes
•••• sap.capire.flights.SupplementTypes.texts
••• sap.capire.flights.Supplements.texts
••• sap.capire.flights.data.SupplementTypes.texts
•• sap.capire.flights.data.Supplements.texts
Skipped: 11
Skipped: 31
...In addition to the generated services.csn file, an index.cds file was added, which you can modify as needed. It won't be overridden on subsequent runs of cds export.
Packaged APIs
The third generated file is package.json:
{
"name": "@capire/xflights-data-service",
"version": "0.1.3"
}{
"name": "@capire/xflights-data-service",
"name": "@capire/xflights-data",
"version": "0.1.3"
}You can modify this file. cds export won't overwrite your changes. In our xflights/xtravels sample, we changed the package name to @capire/xflights-data.
Yet Another CAP Package (YACAP)
The generated output is a complete CAP package. You can add additional files to the ./apis subfolder: models in .cds files, data in .csv files, I18n bundles, or even .js or .java files with custom logic for consumers.
Adding Initial Data and I18n Bundles
You can use these cds export options to add I18n bundles and initial data, which generates files next to the .csn file:
cds export srv/data-service.cds --texts > apis/data-service/_i18n/i18n.properties
> apis/data-service/_i18n/i18n_de.properties
> apis/data-service/_i18n/i18n_fr.propertiescds export srv/data-service.cds --data > apis/data-service/data/sap.capire.flights.data.Flights.csv
> apis/data-service/data/sap.capire.flights.data.Airlines.csv
> apis/data-service/data/sap.capire.flights.data.Airports.csv
> apis/data-service/data/sap.capire.flights.data.Supplements.csvThe .csv data comes from the source application's initial data, filtered and transformed for the exposed entities, including denormalizations and calculated fields. The application actually reads it via an instance of that service.
Plug & Play Config
Use the --plugin option to turn the package into a CAP plugin and benefit from CAP's plug & play configuration features in consuming apps:
cds export srv/data-service.cds --pluginThis would add this to the generated output:
// just a tag file for plug & play{
"name": "@capire/xflights-data",
"version": "0.1.13",
"cds": {
"requires": {
"sap.capire.flights.data": true
}
}
}Publishing APIs
The output of cds export is a valid npm or Maven package, which can be published to any npm-compatible registry, such as the public npmjs.com registry, or private registries like GitHub Packages, Azure Artifacts, or JFrog Artifactory. For example:
npm publish ./apis/data-serviceUsing GitHub Packages ...
Within the capire org, we're publishing to GitHub Packages, which requires you to npm login once like that, prior to publishing:
npm login --scope=@capire --registry=https://npm.pkg.github.comAs for the password, use a 'Personal Access Token (classic)' with the read:packages scope (for retrieving and installing a package). Read more about that in the GitHub Packages docs.
Not using npm registries ...
Instead of publishing to npm registries we can also share packages any other way. For example we could create an archive that we upload to some marketplace like SAP Business Accelerator Hub, or team-internal ones.
For Node.js we'd use npm pack to create installable archives, which would print some output with the last line telling us the filename of the created archive:
npm pack ./apis/data-servicenpm notice ...
npm notice 4.9kB services.csn
npm notice 410B index.cds
npm notice 61B package.json
npm notice ...
capire-xflights-data-0.1.13.tgzWARNING
Not using package registries like npm or Maven also means that you'll loose all their support for semver-based dependency management.
Best Practice: Using Proven Standards
CAP leverages standard and widely adopted package management tools and practices, such as npm and Maven for sharing and distributing reuse packages. This allows you to use established and battle-tested workflows and tools for versioning, publishing, consuming, and upgrading packages. At the same time it allows us to not reinvent those wheels, and focus on what matters most: allowing you to focus on domain, and be as productive as possible.
Importing APIs
On the consumer side, like @capire/xtravels in our sample scenario, we import packaged APIs from CAP and non-CAP sources using npm add and cds import respectively.
Packaged APIs
Import packaged APIs provided by CAP service providers like that:
npm add @capire/xflights-dataThis makes the exported models with all accompanying artifacts available in the target project's node_modules folder. In addition, it adds a respective package dependency to the consuming application's package.json like this:
{...
"dependencies": { ...
"@capire/xflights-data": "0.1.12"
}
}This allows us to update imported APIs later on using standard commands like npm update.
OData APIs
You can also cds import APIs from other sources, such as OData APIs for customer data from SAP S/4 HANA systems:
Get an OData EDMX source, e.g., from SAP Business Accelerator Hub:
Detailed steps through SAP Business Accelerator Hub ...
- Open https://api.sap.com in your browser
- Navigate to > SAP S/4HANA Cloud Public Edition > APIs > OData V2
- Find and open Business Partner (A2X)
- Switch to the API Specification subtab.
- Click the download icon next to OData EDMX to download the
.edmxfile.
Import that to the current project:
cds import ~/Downloads/API_BUSINESS_PARTNER.edmxThis copies the specified .edmx file into the srv/external/ subfolder of your project, and generates a .csn file with the same basename next to it:
srv/external
├── API_BUSINESS_PARTNER.csn
└── API_BUSINESS_PARTNER.edmxAdd option
--as cdsto generate a human-readable.cdsfile instead of.csn.
Import from other APIs
You can use cds import in the same way as for OData to import SAP data products, OpenAPI definitions, AsyncAPI definitions, or from ABAP RFC. For example:
cds import --data-product ...
cds import --odata ...
cds import --openapi ...
cds import --asyncapi ...
cds import --rfc ...Reuse Packages
If you find yourself importing the same APIs every time in new projects, you can create a package that imports the APIs once and reuse it instead. Reusable packages use the same techniques as cds export and provide the same plug & play convenience.
For the XTravels sample, we created the @capire/s4 reuse package as follows:
We started a new CAP project – clone the repository into the
cap/samplesfolder we created in the beginning and open it in VS Code to follow along:shellgit clone https://github.com/capire/s4.git code s4We imported the OData API as outlined above.
shellcds import ~/Downloads/API_BUSINESS_PARTNER.edmxEdited the
cds import-generatedpackage.jsonto look like that:json{ "name": "@capire/s4", "version": "1.0.0", "cds": { "requires": { "sap.capire.s4.business-partner": { "service": "API_BUSINESS_PARTNER", "kind": "odata-v2" } } } }1
2
3
4
5
6
7
8
9
10
11
12Added the following files to expose the imported API in a CAP-idiomatic way:
js// just a tag file for plug & play configuration in consuming appscdsusing from './srv/external/API_BUSINESS_PARTNER'; annotate API_BUSINESS_PARTNER with @cds.external:2;1
2cds// Entry point to allow imports like: using from '@capire/s4'; using from './srv/business-partners';1
2Added some initial data using
cds add data.shellcds add data -o ./srv/external/data -f A_BusinessPartnerFinally published the package to Github Packages.
shellnpm publish
In the consuming project @capire/xtravels we then simply added this package in the same way as we added the @capire/xflights-data package before:
npm add @capire/s4Pre-built Integration Packages
In effect, pre-built integration packages apply the same best practice techniques as the cds export command does when generating Packaged APIs. Such packages can be reused in any CAP project by a simple npm add command, thereby avoiding the need to re-import raw API definitions in each consuming project from scratch. Last but not least, they allow central version management based on npm and Maven.
Integrating Models
With imported APIs, you can now use them in your own models. For example, the XTravels application combines customer data from SAP S/4HANA with travels and flight bookings from xflights. With the integrated models, you can already run the application, as CAP mocks integrations automatically. For real integration, you'll need custom code, which we'll cover later.
AI Agents 'capire' CAP
We can use AI agents to help us analyse and understand our models. Actually, the following sections are based on a response by Claude Sonnet to the question: "Find and explain all references", with the entity definition for the Flights consumption view selected as context.
Consumption Views
Imported APIs often contain more entities and elements than you need. So, before we continue, we first create Consumption Views to capture what you actually want to use, focusing on entities and elements you need close access to.
Create two new files apis/capire/xflights.cds and apis/capire/s4.cds:
using { sap.capire.flights.data as x } from '@capire/xflights-data';
namespace sap.capire.xflights;
@federated entity Flights as projection on x.Flights {
ID, date, departure, arrival, modifiedAt,
airline.icon as icon,
airline.name as airline,
origin.name as origin,
destination.name as destination,
}
@federated entity Supplements as projection on x.Supplements {
ID, type, descr, price, currency, modifiedAt,
}2
3
4
5
6
7
8
9
10
11
12
13
14
using { API_BUSINESS_PARTNER as S4 } from '@capire/s4';
namespace sap.capire.s4;
@federated entity Customers as projection on S4.A_BusinessPartner {
BusinessPartner as ID,
PersonFullName as Name,
LastChangeDate as modifiedAt,
} where BusinessPartnerCategory == 1; // 1 = Person2
3
4
5
6
7
8
The noteworthy aspects here are:
We map names to match our domain, e.g.,
A_Business_Partner->Customers, and choose simpler names for the elements we want to use.For entity
Flightswe flatten data from associations directly into the consumption view. This is another denormalization to make life easier for us in the xtravels app.The namespaces
sap.capire.s4andsap.capire.xflightsreflect the source systems but differ from the original namespaces to avoid name clashes.We add
@federatedannotations, which we'll use later on to automate data federation.
Always use Consumption Views
Even though they are optional, it's a good practice to always define consumption views on top of imported APIs. They declare what you need, enabling automated data federation. They also map imported definitions to your domain by renaming, flattening, or restructuring.
Protocol-specific Limitations
Depending on the service provider and protocols, limitations apply to consumption views. In particular, OData doesn't support denormalization like we used for the Flights view. This works here because xflights also serves the HCQL protocol (see the @hcql annotation in its definition), which is CAP's native protocol.
Associations
With consumption views in place, you can now reference them from your models as if they were local, creating mashups of imported and local definitions.
using { sap.capire.xflights as x } from '../apis/capire/xflights';entity Bookings { // ...
Flight : Association to x.Flights;
}26
27
- Each Booking references a Flight from the external xflights service, which allows us to display flight details alongside bookings.
Associations from Remote
using { sap.capire.xflights as x } from '../apis/capire/xflights';extend x.Flights with columns {
Bookings : Association to many Bookings on Bookings.Flight = $self
}74
75
- Adds a backlink from Flights to Bookings for bidirectional traversal.
Limitations of Remote Extensions
Extensions to remote entities, as shown above, are only possible for elements which would not require changes to the remote service's actual data. This is the case for virtual elements and calculated fields, as well as unmanaged associations, as all foreign keys are local. It's not possible for regular elements or managed associations, though.
Constraints
annotate TravelService.Bookings with { ...
Flight @mandatory {
date @assert: (case
when date not between $self.Travel.BeginDate and $self.Travel.EndDate
then 'ASSERT_BOOKING_IN_TRAVEL_PERIOD'
end);
};
}45
46
47
48
49
50
51
- Adds a constraint to the Flight.date element to ensure that the flight date of a booked Flight falls within the travel period of the associated Travel.
Serving UIs
using { sap.capire.xflights as x } from '../apis/capire/xflights';@fiori service TravelService { ...
@readonly entity Flights as projection on x.Flights;
}17
18
- Exposes the Flights entity in the TravelService for UI consumption. This is required as associations to non-exposed entities would be cut off, which would apply to the Bookings -> x.Flights association if we did not expose x.Flights.
Fiori Annotations
On top of the mashed up models we can add Fiori annotations as usual to serve Fiori UIs – again: as if they were local. For example, following are excerpts of Fiori annotations referring to the A_BusinessPartner entity imported from S/4 (via the Customers consumption view, and the association to that from the local Travels entity).
annotate s4.Customers with @title: '{i18n>Customer}' { ...
ID @title: '{i18n>Customer}' @Common.Text: Name;
}annotate our.Travels with { ...
Customer @title: '{i18n>Customer}' @Common: {
Text: (Customer.Name), TextArrangement: #TextOnly
};
}annotate our.Travels { ...
Customer @Common.ValueList: { CollectionPath: Customers, ... }
}annotate TravelService.Travels with @UI: { ...
SelectionFields: [ (Customer.ID), ... ],
LineItem: [ { Value: (Customer.ID), .... }, ... ],
FieldGroup #Tx: { Data: [ { Value: (Customer.ID) }, ... ]}
}annotate TravelService.Bookings with @UI: { ...
HeaderInfo: { Title: { Value: (Travel.Customer.Name) }, ... },
FieldGroup #GI: { Data: [ { Value: (Travel.Customer.ID) }, ... ]},
}There are similar references to Flights entity from xflights in other parts of the Fiori annotations, which we omit here for brevity.
Mocked Out of the Box
With mashed up models in place, we can run applications in 'airplane mode' without upstream services running. CAP mocks imported services automatically in-process with mock data in the same in-memory database as our own data.
Start the xtravels application locally using
cds watchas usual, and note the output about the integrated services being mocked automatically:shellcds watch1zsh[cds] - mocking sap.capire.s4.business-partner { at: [ '/odata/v4/s4-business-partner' ], decl: 's4/external/API_BUSINESS_PARTNER.csn:7' }zsh[cds] - mocking sap.capire.flights.data { at: [ '/odata/v4/data', '/rest/data', '/hcql/data' ], decl: 'xflights/apis/data-service/services.csn:3' }Open the Fiori UI in the browser -> it displays data from both, local and imported entities, seamlessly integrated as shown in the screenshot below (the data highlighted in green is mocked data from
@capire/s4).

Fast-track Inner-Loop Development → Spawning Parallel Tracks
The mocked-out-of-the-box capabilities of CAP, with remoted services mocked in-process and a shared in-memory database, allows us to greatly speed up development and time to market. For real remote operations there is additional investment required, of course. But the agnostic nature of CAP-level Service Integration also allows you to spawn two working tracks running in parallel: One team to focus on domain and functionality, and another one to work on the integration logic under the hood.
Learn more about mocking and inner loop development in the Inner Loop Development guide.
Integration Logic Required
While everything just works nicely when mocked in-process and with a shared in-memory database, let's move closer to the target setup and use cds mock to run the services to be integrated in separate processes.
First run these commands in two separate terminals:
shellcds mock apis/capire/xflights.cds1shellcds mock apis/capire/s4.cds2Start the xtravels server as usual in a third terminal, and note that it now connects to the other services instead of mocking them:
shellcds watch3zsh[cds] - connect to sap.capire.s4.business-partner > odata { url: 'http://localhost:54476/odata/v4/s4-business-partner' }zsh[cds] - connect to sap.capire.flights.data > hcql { url: 'http://localhost:54475/hcql/data' }Open the Fiori UI in the browser again -> data from the S/4 service is missing now, as we have not yet implemented the required custom code for the actual data integration, the same applies to the flight data from xflights:


Integration Logic
This chapter walks you through the typical use cases and solution patterns that you should be aware of when implementing required integration logic. The following sections do that on the example of CAP Node.js SDK; the same principles and patterns apply to CAP Java, as documented in the CAP Java SDK reference documentation.
Connecting to Remote Services
It all starts with connecting to remote services, which we do like that in the xtravels project:
const s4 = await cds.connect.to ('sap.capire.s4.business-partner')
const xflights = await cds.connect.to ('sap.capire.flights.data')22
The cds.connect.to(<service>) function used here is the single common way to address service instances. It's used for and works the same way for both, local as well as remote services:
for local services, it returns the local service providers – i.e., instances of
cds.ApplicationService, or your application-specific subclases thereof.for remote services, it returns a remote service proxy – i.e., instances of
cds.RemoteService, generically constructed by the client libs.
Agnostic to Location and Protocol
Always use cds.connect.to(<service>) to connect to both local and remote services. Both inherit from the cds.Service base class, which constitutes the uniform interface for consuming CAP services – in turn agnostic to underlying protocols, and agnostic to whether its local or remote at all.
Uniform, Agnostic APIs
The uniform and protocol-agnostic programming interface offered through cds.Service is centered around these methods:
cds.connect.to (<service>)→ connects to remote services, as shown above.srv.run (<query>)→ executes advanced, deep queries with remote services.srv.send (<request>)→ synchronous communication, for all kinds of services.srv.emit (<event>)→ asynchronous communication, via messaging middlewares.srv.on (<event>)→ subscribe event handlers to events from other services.
Here are some typical usages found in the xflights/xtravels sample:
await xflights.run (SELECT.from`Flights`.where`modifiedAt > ${latest}`)
await xflights.send ('POST','BookingCreated', { flight, date, seats })
await this.emit ('Flights.Updated', { flight, date, free_seats }) // this = xflights service
xflights.on ('Flights.Updated', async msg => { ... })2
3
4
- Line 1 – queries the xflights service for updated flights since the last sync
- Line 2 – calls a custom action of the xflights service (synchronously).
- Line 3 – emits asynchronous events from the xflights service.
- Line 4 – subscribes an event handler to events from the xflights service.
The srv.send(<request>) method – and its REST-style derivatives – is the most flexible option, as it allows to send all kinds of requests to all kinds of services – including non-CAP services, and non-OData services, down to very technical services, for which no API schema might exist at all.
The, srv.emit(<event>) method – with srv.on(<event>) on subscribers' side – promotes asynchronous communication via events, which is most recommended for reasons of decoupling and scalability. It requires the target service to be connected via a messaging middleware, though.
The srv.run(<query>) method – and its CRUD-style derivatives – is the most powerful option, and closest to the use cases of data-centric business applications. It requires the target service to support querying, though, like CAP application services, OData services, or GraphQL services.
Choosing the Right Method
Choose the method that best fits your use case and the capabilities of the target service. Prefer srv.run(<query>) for its power and conceptual expressiveness with data-centric operations. Consider srv.emit(<event>) for decoupled, asynchronous communication whenever possible. Retreat to srv.send(<request>) for maximum flexibility only when needed.
Staying at CAP Level
Always stay at CAP level when integrating services, using the uniform and protocol-agnostic Core Service APIs outlined above, combined with CQL as CAP's universal query language. This allows CAP to automate things like protocol translations, data federation, resilience for you, as well as mocking services out of the box, thereby promoting fast inner loops. Only retreat to lower levels when absolutely necessary.
Testing with cds repl
We can use cds repl to experiment the options to send requests and queries to remote services interactively. Do so as follows...
From within the xtravels project's root folder cap/samples/xtravels, start by mocking the remote services in separate terminals, then start xtravels server within cds repl in a third terminal:
cds mock apis/capire/xflights.cdscds mock apis/capire/s4.cdscds repl ./Within the REPL, connect to local and remote services:
const TravelService = await cds.connect.to ('TravelService')
const xflights = await cds.connect.to ('sap.capire.flights.data')
const s4 = await cds.connect.to ('sap.capire.s4.business-partner')Read data directly from the remote A_BusinessPartner entity.
await s4.run (SELECT.from`A_BusinessPartner`.limit (3))
await s4.read`A_BusinessPartner`.limit (3) // shorthandThe variant on line 2 is a convenient shorthand for the one on line 1.
See results output ...
=> [
{
BusinessPartner: '000001',
PersonFullName: 'Mrs. Theresia Buchholm',
LastChangeDate: '2024-01-19',
LastChangeTime: '21:48:32',
BusinessPartnerCategory: '1'
},
{
BusinessPartner: '000002',
PersonFullName: 'Mr. Johannes Buchholm',
LastChangeDate: '2024-01-08',
LastChangeTime: '11:22:01',
BusinessPartnerCategory: '1'
},
{
BusinessPartner: '000003',
PersonFullName: 'Mr. James Buchholm',
LastChangeDate: '2022-11-04',
LastChangeTime: '15:27:46',
BusinessPartnerCategory: '1'
}
]Read the same data via the sap.capire.s4.Customers consumption view:
const { Customers } = cds.entities ('sap.capire.s4')
await s4.read (Customers) .limit (3) See results output ...
=> [
{ ID: '000001', Name: 'Mrs. Theresia Buchholm', modifiedAt: '2024-01-19' },
{ ID: '000002', Name: 'Mr. Johannes Buchholm', modifiedAt: '2024-01-08' },
{ ID: '000003', Name: 'Mr. James Buchholm', modifiedAt: '2022-11-04' }
]Note how field names and structure are adapted to our domain.
See OData requests ...
Watch the log output in the second terminal to see the translated OData requests being received by the remote service, for example:
[odata] - GET /odata/v4/s4-business-partner/A_BusinessPartner {
'$top': '3'
}[odata] - GET /odata/v4/s4-business-partner/A_BusinessPartner {
'$select': 'BusinessPartner,PersonFullName,LastChangeDate',
'$top': '3'
}CRUD some data into remote A_BusinessPartner entity, still via the sap.capire.s4.Customers consumption view:
await s4.insert ({ ID: '123', Name: 'Sherlock' }) .into (Customers)
await s4.create (Customers, { ID: '456', Name: 'Holmes' })
await s4.read`ID, Name` .from (Customers) .where`length(ID) <= 3`
await s4.update (Customers,'123') .with ({ modifiedAt: '2026-01-01' })
await s4.delete (Customers,'123')
await s4.delete (Customers) .where`ID = ${'456'}`Always use Consumption Views
Even when accessing remote services directly, always prefer doing so via consumption views as shown above. They map the remote definitions to your domain, and allow CAP to automatically translate queries accordingly. This includes renaming, flattening, restructuring, as well as filtering out unnecessary data.
Modifying CQNs
Queries in CAP are represented as first-class CQN objects under the hood. When querying remote services, we can inspect and modify those query objects prior to forwarding them to target services for execution.
Let's try that out in cds repl, which we started before.
- Construct and inspect an example of an inbound query:
q1 = SELECT`ID, Name`.from (Customers) .where`length(ID) <= 3`=> cds.ql {
SELECT: {
from: { ref: [ 'sap.capire.s4.Customers' ] },
columns: [
{ ref: [ 'ID' ] },
{ ref: [ 'Name' ] }
],
where: [
{ func:'length', args: [ {ref:['ID']} ] }, '<=', { val:3 }
],
}
}- Create a clone of that query to modify it without changing the original one:
q2 = cds.ql.clone (q1) // get a clone to keep q1 intact- Modify our cloned query as needed. For example, let's replace the existing where clause, and add an order by clause like this:
q2.SELECT.where = cds.ql.predicate`contains (Name,'Astrid')`
q2.orderBy `Name asc`=> cds.ql {
SELECT: { // ... as before ...,
where: [
{ func: 'contains', args: [ {ref:['Name']}, {val:'Astrid'} ] }
],
orderBy: [ {ref:['Name'], sort: 'asc' } ]
},
}- Finally forward / run the modified query:
await s4.run (q2)See results output ...
=> [
{ ID: '000096', Name: 'Mrs. Astrid Detemple' },
{ ID: '000037', Name: 'Mrs. Astrid Gutenberg' },
{ ID: '000164', Name: 'Mrs. Astrid Hoffen' },
{ ID: '000399', Name: 'Mrs. Astrid Kramer' },
{ ID: '000087', Name: 'Mrs. Astrid Martin' },
{ ID: '000527', Name: 'Mrs. Astrid Sommer' },
{ ID: '000203', Name: 'Mrs. Astrid Waldmann' }
]Powerful Query Adaptation
Modifying queries prior to forwarding them to remote services is a powerful technique to implement advanced integration scenarios. For example, you can adapt queries to the capabilities of target services, implement custom filtering, paging, or sorting logic, or even split and merge queries across multiple services.
First-Class Query Objects
On a side note: We leverage key principles of first-class objects here, as known from functional programming and dynamic languages: As queries are represented as first-class CQN objects, we can construct and manipulate them programmatically at runtime, pass them as arguments, and return them from functions. And, not the least, this opens the doors for things like higher-order queries, query delegation – e.g. push down to databases –, and late materialization.
Always Clone Before Modifying
As always, great power comes with great responsibility: Ensure to cds.ql.clone CQNs before modifying them, as they are shared across the entire request processing pipeline. Failing to do so may lead to unexpected side effects and hard-to-debug issues. And CAP runtimes can only optimize for immutable CQNs.
Data Federation
There are many scenarios where data from remote services needs to be in close access locally. For example, in the xtravels app we want to display lists of flight details alongside bookings in Fiori UIs. This requires joining data from the local Bookings entity with data from the remote Flights entity.
Relying on live calls to remote services per row is clearly not an option. Instead, we'd rather ensure that data required in close access is really available locally, so it can be joined with own data using SQL JOINs. This is what data federation is all about.
Basic Implementation
Following would be a basic implementation for replicating flights data from the remote xflights service into local database tables of the xtravels app:
- Annotate your consumption views with
@cds.persistence.tableto turn them into tables to persist replicated data locally:
// turn into table to persist replicated data
annotate x.Flights with @cds.persistence.table;- Implement logic to replicate updated data, for example like that:
const xflight = await cds.connect.to ('sap.capire.flights.data')
const {Flights} = cds.entities ('sap.capire.xflights')
let {latest} = await SELECT.one`max(modifiedAt) as latest`.from (Flights)
let touched = await xflight.read (Flights).where`modifiedAt > ${latest||0}`
if (touched.length) await UPSERT (touched).into (Flights)Generic Implementation
While the above is a valid implementation for data replication, it is specific to the Flights entity, which means we would need to write similar code for each entity we want to replicate. Therefore, we actually implemented a more generic solution for data federation in xtravels, which automatically kicks in on any entity tagged with the @federated annotated, which we already used in our consumption views:
@federated entity Flights as projection on x.Flights { ... }
@federated entity Supplements as projection on x.Supplements { ... }@federated entity Customers as projection on S4.A_BusinessPartner { ... }Besides the advantages of reusability and maintainability, this also allows us to easily add new entities for data federation just by annotating them with @federated, without the need to write any custom code at all. The projections defined in such @federated consumption views also declare exactly what data needs to be in close access, and what not, thereby avoiding overfetching.
Learn more about that generic solution in the CAP-level Data Federation guide.
When to Use Data Federation
Data federation is essential when remote data is needed in close access for joins with local data, filtering, or sorting operations. It drastically improves read performance and reduces latency, as well as overall load. It also increases resilience and high availability by reducing dependencies on other services.
Delegation
Even with data federation in place, there are still several scenarios where we need to reach out to remote services on demand. Value helps are a prime example for that; for example, to select Customers from a drop-down list when creating new travels. Although we could serve that from replicated data as well, this would require replicating all relevant customer data locally, which is often overkill.
The code below shows how we simply delegate value help requests for Customers in xtravels to the connected S/4 service:
this.on ('READ', Customers, req => s4.run (req.query))The event handler intercepts all direct READ requests to the Customers entity, and just forwards the query as-is to the connected S/4 service.
Try this in cds repl ...
cds mock apis/capire/s4.cdscds repl ./Within the cds repl session in the second terminal, run this:
await TravelService.read`ID, Name`.from`Customers`.limit(3)This issues a READ request to the local TravelService.Customers entity, which is intercepted by the above event handler, and delegated to the remote S/4 service. The result comes back translated to the structure of the Customers consumption view:
=> [
{ ID: '000001', Name: 'Mrs. Theresia Buchholm' },
{ ID: '000002', Name: 'Mr. Johannes Buchholm' },
{ ID: '000003', Name: 'Mr. James Buchholm' }
]See the log output of in the first terminal where we cds mocked the S/4 service to observe the translated OData request being received by the remote service:
[odata] - GET /odata/v4/s4-business-partner/A_BusinessPartner {
'$select': 'BusinessPartner,PersonFullName',
'$top': '3'
}Automatic Query Translation
Note that for the handler above, incoming requests always refer to:
- the
TravelService.Customersentity – which is a view on:- the
sap.capire.s4.Customersentity – which in turn is a view on:- the
A_BusinessPartnerremote entity.
- the
- the
In effect, we are delegating a query to the S/4 service, which refers to an entity actually not known to that remote service. How could that work at all?
It works because we fuelled the CAP runtime with CDS models, so the generic handlers detect such situations, and automatically translate delegated queries into valid queries targeted to underlying remote entities – i.e. A_BusinessPartner in our example. When doing so, all column references in select clauses, where clauses, etc., are translated and delegated as well, and the results' structure transformed back to that of the original target – i.e., TravelService.Customers above.
Navigation
Automatic translation of delegated queries, as shown above, has limitations when navigations and expands are involved. Let's explore those limitations and how to deal with them on the example of the Bookings -> Flights association.
Try running the following query in cds repl, with the xflights service mocked in a separate process, as before:
const { Bookings } = cds.entities ('sap.capire.travels')await SELECT.from (Bookings) .where`Flight.origin like '%Ken%'`- With data federation in place, this would work (if all flight data had been replicated).
- Without data federation, though, this would fail with a runtime error.
For that to really work cross-service – that is, without data federation, or bypassing it – we'd have to split the query, manually dispatch the parts to involved services, and correlate results back, for example, like this:
await SELECT.from (Bookings) .where`Flight.ID in ${(
await xflights.read`ID`.from`Flights`.where`origin.name like '%Ken%'`
).map (f => f.ID)}`Details
The above can also be written like that, of course:
const flights = await xflights.read`ID`.from`Flights`.where`origin.name like '%Ken%'`
const flightIDs = flights.map (f => f.ID)
await SELECT.from (Bookings) .where`Flight.ID in ${flightIDs}`What is 'Navigation'?
The term 'navigation' commonly refers to traversing associations between entities in queries. In CAP, this is typically expressed using path expressions along (chains of) associations – e.g., flight.origin.name –, which can show up in all query clauses (select, from, where, order by, and group by).
Expands
Similar to navigations, expands across associations also require special handling when we cannot serve them from federated data. Try running the following query in cds repl, with the xflights service mocked in a separate process, as before:
await SELECT.from (Bookings) .columns`{
Flight { ID, date, destination }
}` .where`exists Flight` .limit(3)See results output ...
=> [
{ Flight: { ID: 'SW1537', date: '2023-08-04', destination: 'Miami International Airport' } },
{ Flight: { ID: 'SW1537', date: '2023-08-04', destination: 'Miami International Airport' } },
{ Flight: { ID: 'SW1537', date: '2023-08-04', destination: 'Miami International Airport' } }
]To achieve the same without data federation, we'd have to manually fetch nested data from the remote service for each row, and fill it into the outer results, for example like this:
await SELECT.from(Bookings).columns`Flight_ID, Flight_date`.limit(3)
.then (all => Promise.all (all.map (async b => ({
Flight: await xflights.read`ID, date, destination.name as destination`
.from`Flights`.where`ID = ${b.Flight_ID} and date = ${b.Flight_date}`
}))))We can do similar things for expands across associations from remote data to local ones, for example like that:
const { Customers } = cds.entities ('sap.capire.s4')
const { Travels } = cds.entities ('sap.capire.travels')
await s4.read(Customers).columns`{ ID, Name }`
.then (all => Promise.all (all.map (async c => Object.assign (c, {
Travels: await SELECT`ID`.from(Travels).where`Customer.ID = ${c.ID}`
}))))Outboxed Emits
Use transactional outbox for write operations, which you want to take place reliably, but don't need the results in your current execution context. As in this event hander example:
const xflights_ = cds.outboxed (xflights)
this.after ('SAVE', Travels, ({ Bookings=[] }) => {
return Promise.all (Bookings.map (booking => {
let { Flight_ID: flight, Flight_date: date } = booking
return xflights_.send ('POST', 'BookingCreated', { flight, date })
}))
})31
32
33
34
35
36
- Line 29 – We create an outboxed version of the connected xflights service.
- Line 34 – We use that outboxed service to send events to the xflights service.
This creates ultimate resilience, as the events are stored in a local outbox table within the same transaction as the SAVE operation on Travels. A separate process then takes care of reliably forwarding those events to the xflights service, retrying in case of failures, etc.
Learn More
CAP-level Data Federation – Explore different patterns and strategies for data federation in CAP applications.
Inner Loop Development – Understand how to develop and test integrated applications efficiently using CAP's inner loop development features.