Search

    Consuming Services

    Learn how to use uniform APIs to consume local or remote services.

    Content

    Introduction

    If you want to use data from other services or you want to split your application into multiple microservices, you need a connection between those services, we call them remote services. As everything in CAP is a service, remote services are modeled the same way as internal services using CDS.

    CAP supports the service consumption with dedicated APIs to import service definitions, query remote services, mash up services, and work locally as long as possible.

    Feature Overview

    For outbound remote service consumption, the following features are supported:

    Tutorials and Examples

    Most snippets in this guide are from the Build an Application End-to-End using CAP, Node.js, and VS Code tutorial. Remaining snippets originate from the other examples.

    Example Description
    Build an Application End-to-End using CAP, Node.js, and VS Code End-to-end Tutorial, Node.js, SAP S/4HANA, Delegate calls, Mashups, SAP Fiori, Value Help
    Capire Bookshop (Fiori) Example, Node.js, CAP-to-CAP
    Example Application (Node.js) Complete application from the end-to-end Tutorial
    Example Application (Java) Complete application from the end-to-end Tutorial

    We recommend doing the Build an Application End-to-End using CAP, Node.js, and VS Code tutorial to get the experience of an end-to-end scenario using SAP BTP. A part of this end-to-end tutorial are tutorials showing how to add and consume remote services.

    Define Scenario

    Before you start your implementation, you should define your scenario. Answering the following questions gets you started:

    • What services (remote/CAP) are involved?
    • How do they interact?
    • What needs to be displayed on the UI?

    You’ve all your answers and know your scenario, go on reading about external service APIs, getting an API definition from the SAP API Business Hub or from a CAP project, and importing an API definition to your project.

    Sample Scenario from End-to-End Tutorial

    The risk management use case of the previously mentioned tutorial shows you one possible scenario:

    User Story
    A company wants to ensure that goods are only sourced from suppliers with acceptable risks. There shall be a software system, that allows a clerk to maintain risks for suppliers and their mitigations. The system shall block the supplier used if risks can’t be mitigated.

    The application is an extension for SAP S/4HANA. It deals with risks and mitigations that are local entities in the application and suppliers that are stored in SAP S/4HANA Cloud. The application helps to reduce risks associated with suppliers by automatically blocking suppliers with a high risk using a remote API Call.

    Integrate

    The user picks a supplier from the list. That list is coming from the remote system and exposed by the CAP application. Then the user does a risk assessment. Additional supplier data, like name and blocked status, should be displayed on the UI as well, by integrating the remote supplier service into the local risk service.

    Extend

    It should be also possible to search for suppliers and show the associated risks by extending the remote supplier service with the local risk service and its risks.

    Get and Import an External Service API

    To communicate to remote services, CAP needs to know their definitions. Having the definitions in your project allows you to mock them during design time.

    These definitions are usually made available by the service provider. As they aren’t defined within your application, but imported from outside, they’re called external service APIs in CAP. Service APIs can be provided in different formats. Currently, EDMX files for OData V2 and V4 are supported.

    Get a Service API from SAP API Business Hub

    The SAP API Business Hub provides many relevant APIs from SAP. You can download API specifications in different formats. If available, use the EDMX format. The EDMX format describes OData interfaces.

    To download the Business Partner API (A2X) from SAP S/4HANA Cloud, go to section API Resources, select API Specification, and download the EDMX file.

    Get more details in the end-to-end tutorial.

    Get a Service API for a Remote CAP Service

    We recommend using EDMX as exchange format. Export a service API to EDMX:

    ❗ Warning
    The export-import cycle is the way to go for now. It is under investigation to improve this procedure.

    cds compile srv -s OrdersService -2 edmx >OrdersService.edmx
    

    You can try it with the orders sample in cap/samples.

    By default, CAP works with OData V4 and the EDMX export is in this protocol version as well. The cds compile command offers options for other OData versions and flavors, call cds help compile for more information.

    Don’t just copy the CDS file for a remote CAP service, for example from a different application. There are issues to use them to call remote services:
    - The effective service API depends on the used protocol.
    - CDS files often use includes, which can’t be resolved anymore.
    - CAP creates unneeded database tables and views for all entities in the file.

    Import API Definition

    Import the API to your project using cds import.

    cds import ~/Downloads/API_BUSINESS_PARTNER.edmx --keep-namespace --as cds
    
    Option Description
    --keep-namespace Keep the namespace of the existing service. Otherwise, the namespace is changed to the file’s base name when converting the file to CSN.
    Note: In this example, it would be still API_BUSINESS_PARTNER.
    --as cds The import creates a CDS file (for example API_BUSINESS_PARTNER.cds) instead of a CSN file.

    This adds the API in CSN format to the srv/external folder and also copies the EDMX file into that folder. Additionally, for Node.js it adds the API as an external OData service to your package.json. You use this declaration later to connect to the remote service using a destination.

    "cds": {
        "requires": {
            "API_BUSINESS_PARTNER": {
                "kind": "odata-v2",
                "model": "srv/external/API_BUSINESS_PARTNER",
            }
        }
    }
    

    The kind odata-v2 is set when importing EDMX definitions of OData V2 format. When importing OData V4, the kind odata is set, which is an alias for kind odata-v4.

    Always use OData V4 (odata) when calling another CAP service.

    For Java you need to configure remote services in Spring Boot’s application.yaml:

    spring:
      config.activate.on-profile: cloud
    cds:
      remote.services:
      - name: "API_BUSINESS_PARTNER"
        destination:
          type: "odata-v2"
    

    To work with remote services, add the following dependency to your Maven project:

    <dependency>
        <groupId>com.sap.cds</groupId>
        <artifactId>cds-feature-remote-odata</artifactId>
        <scope>runtime</scope>
    </dependency>
    

    Learn about all cds.remote.services configuration possibilities.

    Local Mocking

    When developing your application, you can mock the remote service.

    Add Mock Data

    As for any other CAP service, you can add mocking data. The CSV file needs to be added to the srv/external/data folder for Node.js. For Java, use the db/data folder.

    BusinessPartner;BusinessPartnerFullName;BusinessPartnerIsBlocked
    1004155;Williams Electric Drives;false
    1004161;Smith Batteries Ltd;false
    1004100;Johnson Automotive Supplies;true
    

    Find this source in the end-to-end Tutorial

    Get more details in the end-to-end tutorial.

    Run Local with Mocks

    Start your project with the imported service definition.

    Node.js:

    cds watch
    

    Java:

    mvn spring-boot:run
    

    The service is automatically mocked, as you can see in the log output on server start.

    Example output for Node.js:

    ...
    
    [cds] - model loaded from 8 file(s):
    
      ...
      ./srv/external/API_BUSINESS_PARTNER.cds
      ...
    
    [cds] - connect using bindings from: { registry: '~/.cds-services.json' }
    [cds] - connect to db > sqlite { database: ':memory:' }
     > filling sap.ui.riskmanagement.Mitigations from ./db/data/sap.ui.riskmanagement-Mitigations.csv
     > filling sap.ui.riskmanagement.Risks from ./db/data/sap.ui.riskmanagement-Risks.csv
     > filling API_BUSINESS_PARTNER.A_BusinessPartner from ./srv/external/data/API_BUSINESS_PARTNER-A_BusinessPartner.csv
    /> successfully deployed to sqlite in-memory db
    
    [cds] - serving RiskService { at: '/service/risk', impl: './srv/risk-service.js' }
    [cds] - mocking API_BUSINESS_PARTNER { at: '/api-business-partner' }
    
    [cds] - launched in: 1.104s
    [cds] - server listening on { url: 'http://localhost:4004' }
    [ terminate with ^C ]
    

    Mock Associations

    You can’t get data from associations of a mocked service out of the box.

    The associations of imported services lack information how to look up the associated records. This missing relation is expressed with an empty key definition at the end of the association declaration in the CDS model ({ }).

    entity API_BUSINESS_PARTNER.A_BusinessPartner {
      key BusinessPartner : LargeString;
      BusinessPartnerFullName : LargeString;
      BusinessPartnerType : LargeString;
    
      ...
    
      to_BusinessPartnerAddress :
        association to many API_BUSINESS_PARTNER.A_BusinessPartnerAddress {  };
    };
    
    entity API_BUSINESS_PARTNER.A_BusinessPartnerAddress {
      key BusinessPartner : String(10);
      key AddressID : String(10);
    
      ...
    };
    

    To mock an association, you’ve to modify the imported file. Before doing any modifications, create a local copy and add it to your source code management system.

    cp srv/external/API_BUSINESS_PARTNER.cds srv/external/API_BUSINESS_PARTNER-orig.cds
    git add srv/external/API_BUSINESS_PARTNER-orig.cds
    ...
    

    Import the CDS file again, just using a different name:

    cds import ~/Downloads/API_BUSINESS_PARTNER.edmx --keep-namespace \
        --as cds --out srv/external/API_BUSINESS_PARTNER-new.cds
    

    Add an on condition in API_BUSINESS_PARTNER-new.cds to express the relation:

      to_BusinessPartnerAddress :
        association to many API_BUSINESS_PARTNER.A_BusinessPartnerAddress
        on to_BusinessPartnerAddress.BusinessPartner = BusinessPartner;
    

    Don’t add any keys or remove empty keys, which would change it to a managed association. Added fields aren’t known in the service and lead to runtime errors.

    Use a 3-way merge tool to takeover your modifications, check it and overwrite the previous unmodified file with the newly imported file:

    git merge-file API_BUSINESS_PARTNER.cds \
                   API_BUSINESS_PARTNER-orig.cds \
                   API_BUSINESS_PARTNER-new.cds
    mv API_BUSINESS_PARTNER-new.cds API_BUSINESS_PARTNER-orig.cds
    

    To prevent accidental loss of modifications, the cds import --as cds command refuses to overwrite modified files based on a “checksum” that is included in the file.

    Mock Remote Service as OData Service (Node.js)

    As shown previously you can run one process including a mocked external service. However, this mock doesn’t behave like a real external service. The communication happens in-process and doesn’t use HTTP or OData. For a more realistic testing, let the mocked service run in a separate process.

    First start the CAP application with the mocked remote service only:

    cds mock API_BUSINESS_PARTNER
    

    If the startup is completed, run cds watch in the same project from a different terminal:

    cds watch
    

    CAP tracks locally running services. The service API_BUSINESS_PARTNER, which you started as mocked, is registered in file ~/.cds-services.json. cds watch searches for running services in that file and connects to them.

    Node.js only supports OData V4 protocol and so does the mocked service. There might be still some differences to the real remote service if it uses a different protocol, but it’s much closer to it than using only one instance. In the console output, you can also easily see how the communication between the two processes happens.

    Mock Remote Service as OData Service (Java)

    You configure CAP to do OData and HTTP requests for a mocked service instead of doing it in-process. Configure a new Spring Boot profile (for example mocked):

    application.yaml:

    spring:
      config.activate.on-profile: mocked
    cds:
      application.services:
      - name: API_BUSINESS_PARTNER-mocked
        model: API_BUSINESS_PARTNER
        serve.path: API_BUSINESS_PARTNER
      remote.services:
      - name: API_BUSINESS_PARTNER
        destination:
          name: "s4-business-partner-api-mocked"
    

    The profile exposes the mocked service as OData service and defines a destination to access the service. The destination just points to the CAP application itself. You need to implement some Java code for this:

    DestinationConfiguration.java:

    @EventListener
    void applicationReady(ApplicationReadyEvent ready) {
      int port = Integer.valueOf(environment.getProperty("local.server.port"));
      DefaultHttpDestination mockDestination = DefaultHttpDestination
          .builder("http://localhost:" + port)
          .name("s4-business-partner-api-mocked").build();
    
      DefaultDestinationLoader loader = new DefaultDestinationLoader();
      loader.registerDestination(mockDestination);
      DestinationAccessor.prependDestinationLoader(loader);
    }
    

    Now, you just need to run the application with the new profile:

    mvn spring-boot:run -Dspring-boot.run.profiles=default,mocked
    

    When sending a request to your CAP application, for example the Suppliers entity, it is transformed to the request for the mocked remote service and requested from itself as a OData request. Therefore, you’ll see two HTTP requests in your CAP application’s log.

    For example:

    http://localhost:8080/service/risk/Suppliers

    2021-09-21 15:18:44.870 DEBUG 34645 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/service/risk/Suppliers", parameters={}
    ...
    2021-09-21 15:18:45.292 DEBUG 34645 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : GET "/API_BUSINESS_PARTNER/A_BusinessPartner?$select=BusinessPartner,BusinessPartnerFullName,BusinessPartnerIsBlocked&$top=1000&$skip=0&$orderby=BusinessPartner%20asc&sap-language=de&sap-valid-at=2021-09-21T13:18:45.211722Z", parameters={masked}
    ...
    2021-09-21 15:18:45.474 DEBUG 34645 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed 200 OK
    2021-09-21 15:18:45.519 DEBUG 34645 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed 200 OK
    

    Try out the example application.

    Execute Queries

    You can send requests to remote services using CAP’s powerful querying API.

    Execute Queries with Node.js

    Connect to the service before sending a request, as usual in CAP:

    const bupa = await cds.connect.to('API_BUSINESS_PARTNER');
    

    Then execute your queries using the Querying API:

    const { A_BusinessPartner } = bupa.entities;
    const result = await bupa.run(SELECT(A_BusinessPartner).limit(100));
    

    We recommend limiting the result set and avoid the download of large data sets in a single request. You can limit the result as in the example: .limit(100).

    Many features of the querying API are supported for OData services. For example, you can resolve associations like this:

    const { A_BusinessPartner } = bupa.entities;
    const result = await bupa.run(SELECT.from(A_BusinessPartner, bp => {
        bp('BusinessPartner'),
        bp.to_BusinessPartnerAddress(addresses => {
          addresses('*')
        })
      }).limit(100));
    

    Learn more about querying API examples.

    Learn more about supported querying API features.

    Execute Queries with Java

    You can use dependency injection to get access to the remote service:

    @Autowired
    @Qualifier(ApiBusinessPartner_.CDS_NAME)
    CqnService bupa;
    

    Then execute your queries using the Querying API:

    CqnSelect select = Select.from(ABusinessPartner_.class).limit(100);
    List<ABusinessPartner> businessPartner = bupa.run(select).listOf(ABusinessPartner.class);
    

    Learn more about querying API examples.

    Learn more about supported querying API features.

    Model Projections

    External service definitions, like generated CDS or CSN files during import, can be used as any other CDS definition, but they don’t generate database tables and views unless they are mocked.

    It’s best practice to use your own “interface” to the external service and define the relevant fields in a projection in your namespace. Your implementation is then independent of the remote service implementation and you request only the information that you require.

    using {  API_BUSINESS_PARTNER as bupa } from '../srv/external/API_BUSINESS_PARTNER';
    
    entity Suppliers as projection on bupa.A_BusinessPartner {
      key BusinessPartner as ID,
      BusinessPartnerFullName as fullName,
      BusinessPartnerIsBlocked as isBlocked,
    }
    

    As the example shows, you can use field aliases as well.

    Learn more about supported features for projections.

    Execute Queries on Projections to a Remote Service

    Here it’s shown with Node.js.

    Connect to the service before sending a request, as usual in CAP:

    const bupa = await cds.connect.to('API_BUSINESS_PARTNER');
    

    Then execute your queries:

    const suppliers = await bupa.run(SELECT(Suppliers).where({ID}));
    

    CAP resolves projections and does the required mapping, similar to databases.

    A brief explanation, based on the previous query, what CAP does:

    • Resolves the Suppliers projection to the external service interface API_BUSINESS_PARTNER.A_Business_Partner.
    • The where condition for field ID will be mapped to the BusinessPartner field of A_BusinessPartner.
    • The result is mapped back to the Suppliers projection, so that values for the BusinessPartner field are mapped back to ID.

    This makes it convenient to work with external services.

    Building Custom Requests with Node.js

    If you can’t use the querying API, you can craft your own HTTP requests using send:

    bupa.send({
      method: 'PATCH',
      path: A_BusinessPartner,
      data: {
        BusinessPartner: 1004155
        BusinessPartnerIsBlocked: true
      }
    })
    

    Learn more about the send API.

    Building Custom Requests with Java

    For Java, you can use the HttpClient API to implement your custom requests. The API is enhanced by the SAP Cloud SDK to support destinations.

    Learn more about using the HttpClient Accessor.

    Learn more about using destinations.

    Integrate and Extend

    By creating projections on remote service entities and using associations, you can create services that combine data from your local service and remote services.

    What you need to do depends on the scenarios and how your remote services should be integrated into, as well as extended by your local services.

    Expose Remote Services

    To expose a remote service entity, you add a projection on it to your CAP service:

    using {  API_BUSINESS_PARTNER as bupa } from '../srv/external/API_BUSINESS_PARTNER';
    
    extend service RiskService with {
      entity BusinessPartners as projection on bupa.A_BusinessPartner;
    }
    

    CAP automatically tries to delegate queries to database entities, which don’t exist as you’re pointing to an external service. That behavior would produce an error like this:

    <error xmlns="http://docs.oasis-open.org/odata/ns/metadata">
    <code>500</code>
    <message>SQLITE_ERROR: no such table: RiskService_BusinessPartners in: SELECT BusinessPartner, Customer, Supplier, AcademicTitle, AuthorizationGroup, BusinessPartnerCategory, BusinessPartnerFullName, BusinessPartnerGrouping, BusinessPartnerName, BusinessPartnerUUID, CorrespondenceLanguage, CreatedByUser, CreationDate, (...)  FROM RiskService_BusinessPartner ALIAS_1 ORDER BY BusinessPartner COLLATE NOCASE ASC LIMIT 11</message>
    </error>
    

    To avoid this error, you need to handle projections. Write a handler function to delegate a query to the remote service and run the incoming query on the external service.

    Node.js:

    module.exports = cds.service.impl(async function() {
      const bupa = await cds.connect.to('API_BUSINESS_PARTNER');
    
      this.on('READ', 'BusinessPartners', req => {
          return bupa.run(req.query);
      });
    });
    

    Get more details in the end-to-end tutorial.

    Java:

    @Component
    @ServiceName(RiskService_.CDS_NAME)
    public class RiskServiceHandler implements EventHandler {
      @Autowired
      @Qualifier(ApiBusinessPartner_.CDS_NAME)
      CqnService bupa;
    
      @On(entity = BusinessPartners.CDS_NAME)
      Result readSuppliers(CdsReadEventContext context) {
        return bupa.run(context.getCqn());
      }
    }
    

    If you receive 404 errors, check if the request contains fields that don’t exist in the service and start with the name of an association. cds import adds now an empty keys declaration ({ }) to each association. Without this declaration, foreign keys for associations are generated in the runtime model, that don’t exist in the real service. To solve this problem, you need to reimport the external service definition using cds import.

    This works when accessing the entity directly. Additional work is required to support navigation and expands from or to a remote entity.

    Instead of exposing the remote service’s entity unchanged, you can model your own projection. For example, you can define a subset of fields and change their names.

    CAP does the magic that maps the incoming query, according to your projections, to the remote service and maps back the result.

    using {  API_BUSINESS_PARTNER as bupa } from '../srv/external/API_BUSINESS_PARTNER';
    
    extend service RiskService with {
      entity Suppliers as projection on bupa.A_BusinessPartner {
        key BusinessPartner as ID,
        BusinessPartnerFullName as fullName,
        BusinessPartnerIsBlocked as isBlocked
      }
    }
    
    module.exports = cds.service.impl(async function() {
      const bupa = await cds.connect.to('API_BUSINESS_PARTNER');
    
      this.on('READ', 'Suppliers', req => {
          return bupa.run(req.query);
      });
    });
    

    Learn more about queries on projections to remote services.

    Expose Remote Services with Associations

    It’s possible to expose associations of a remote service entity. You can adjust the projection for the association target and change the name of the association:

    using {  API_BUSINESS_PARTNER as bupa } from '../srv/external/API_BUSINESS_PARTNER';
    
    extend service RiskService with {
      entity Suppliers as projection on bupa.A_BusinessPartner {
        key BusinessPartner as ID,
        BusinessPartnerFullName as fullName,
        BusinessPartnerIsBlocked as isBlocked,
        to_BusinessPartnerAddress as addresses: redirected to SupplierAddresses
      }
    
      entity SupplierAddresses as projection on bupa.A_BusinessPartnerAddress {
          BusinessPartner as bupaID,
          AddressID as ID,
          CityName as city,
          StreetName as street,
          County as county
      }
    }
    

    As long as the association is only resolved using expands (for example .../risk/Suppliers?$expand=addresses), a handler for the source entity is sufficient:

    this.on('READ', 'Suppliers', req => {
        return bupa.run(req.query);
    });
    

    If you need to resolve the association using navigation or request it independently from the source entity, add a handler for the target entity as well:

    this.on('READ', 'SupplierAddresses', req => {
        return bupa.run(req.query);
    });
    

    As usual, you can put two handlers into one handler matching both entities:

    this.on('READ', ['Suppliers', 'SupplierAddresses'], req => {
        return bupa.run(req.query);
    });
    

    Mashing up with Remote Services

    You can combine local and remote services using associations. These associations need manual handling, because of their different data sources.

    Integrate Remote into Local Services

    Use managed associations from local entities to remote entities:

    @path: 'service/risk'
    service RiskService {
      entity Risks : managed {
        key ID      : UUID  @(Core.Computed : true);
        title       : String(100);
        prio        : String(5);
        supplier    : Association to Suppliers;
      }
    
      entity Suppliers as projection on BusinessPartner.A_BusinessPartner {
          key BusinessPartner as ID,
          BusinessPartnerFullName as fullName,
          BusinessPartnerIsBlocked as isBlocked,
      };
    }
    

    Extend a Remote by a Local Service

    You can augment a view with a new association, if the required fields for the on conditions are present in the remote service. The use of managed associations isn’t possible, because this requires to create new fields in the remote service.

    Projections don’t allow to add associations. Use views. Add the association in a mixin:

    view Suppliers as select from bupa.A_BusinessPartner mixin {
        risks : association to many Risks on risks.supplier.ID = $projection.ID;
    } into {
        key BusinessPartner as ID,
        BusinessPartnerFullName as fullName,
        BusinessPartnerIsBlocked as isBlocked,
        risks
    };
    

    The $projection placeholder is used to access the fields of the projection, similar to the $self in entity definitions.

    Handle Mashups with Remote Services

    Depending on how the service is accessed, you need to support direct requests, navigation, or expands. CAP resolves those three request types only for service entities that are served from the database. When crossing the boundary between database and remote sourced entities, you need to take care of those requests.

    The list of required implementations for mashups explains the different combinations.

    Handle Expands Across Local and Remote Entities

    Expands add data from associated entities to the response. For example, for a risk, you want to display the suppliers name instead of just the technical ID. But this property is part of the (remote) supplier and not part of the (local) risk.

    Get more details in the end-to-end tutorial.

    To handle expands, you need to add a handler for the main entity:

    1. Check if a relevant $expand column is present.
    2. Remove the $expand column from the request.
    3. Get the data for the request.
    4. Execute a new request for the expand.
    5. Add the expand data to the returned data from the request.

    Example of a CQN request with an expand:

    {
      "from": { "ref": [ "RiskService.Suppliers" ] },
      "columns": [
        { "ref": [ "ID" ] },
        { "ref": [ "fullName" ] },
        { "ref": [ "isBlocked" ] }
        { "ref": [ "risks" ] },
        { "expand": [
          { "ref": [ "ID" ] },
          { "ref": [ "title" ] },
          { "ref": [ "descr" ] },
          { "ref": [ "supplier_ID" ] }
        ] }
      ]
    }
    

    See an example how to handle expands in Node.js.

    See an example how to handle expands in Java.

    Expands across local and remote can cause stability and performance issues. For a list of items, you need to collect all IDs and sent it to the database or the remote system. This can become long and may exceed the limits of a URL string in case of OData. Do you really need expands for a list of items?

    /service/risk/Risks?$expand=supplier
    

    Or is it sufficient for single items?

    /service/risk/Risks(545A3CF9-84CF-46C8-93DC-E29F0F2BC6BE)/?$expand=supplier
    

    Consider to reject expands if it’s requested on a list of items.

    Handle Navigations Across Local and Remote Entities

    Navigations allow to address items via an association from a different entity:

    /service/risks/Risks(20466922-7d57-4e76-b14c-e53fd97dcb11)/supplier
    

    The CQN consists of a from condition with 2 values for ref. The first ref selects the record of the source entity of the navigation. The second ref selects the name of the association, to navigate to the target entity.

    {
      "from": {
        "ref": [ {
            "id": "RiskService.Risks",
            "where": [
              { "ref": [ "ID" ] }
              "=",
              { "val": "20466922-7d57-4e76-b14c-e53fd97dcb11" }
            ] },
          "supplier"
        ]
      },
      "columns": [
        { "ref": [ "ID" ] },
        { "ref": [ "fullName" ] },
        { "ref": [ "isBlocked" ] }
      ],
      "one": true
    }
    

    To handle navigations, you need to check in your code if the from.ref object contains 2 elements. Be aware, that for navigations the handler of the target entity is called.

    If the association’s on condition equals the key of the source entity, you can directly select the target entity using the key’s value. You find the value in the where block of the first from.ref entry.

    Otherwise, you need to select the source item using that where block and take the required fields for the associations on condition from that result.

    See an example how to handle navigations in Node.js.

    See an example how to handle navigations in Java.

    Limitations and Feature Matrix

    Required Implementations for Mashups

    You need additional logic, if remote entities are in the game. The following table shows what is required. “Local” is a database entity or a projection on a database entity.

    Request Example Implementation
    Local (including navigations and expands) /service/risks/Risks Handled by CAP
    Local: Expand remote /service/risks/Risks?$expand=supplier Delegate query w/o expand to local service and implement expand.
    Local: Navigate to remote /service/risks(...)/supplier Implement navigation and delegate query target to remote service.
    Remote (including navigations and expands to the same remote service) /service/risks/Suppliers Delegate query to remote service
    Remote: Expand local /service/risks/Suppliers?$expand=risks Delegate query w/o expand to remote service and implement expand.
    Remote: Navigate to local /service/Suppliers(...)/risks Implement navigation, delegate query for target to local service

    Transient Access vs. Replication

    The Integrate and Extend chapter shows only techniques for transient access.

    The following matrix can help you to find the best approach for your scenario:

    Feature Transient Access Replication
    Filtering on local or remote fields 1 Possible Possible
    Filtering on local and remote fields 2 Not possible Possible
    Relationship: Uni-/Bidirectional associations Possible Possible
    Relationship: Flatten Not possible Possible
    Evaluate user permissions in remote system Possible Requires workarounds 3
    Data freshness Live data Outdated until replicated
    Performance Degraded 4 Best


    1 It’s not required to filter both, on local and remote fields, in the same request.
    2 It’s required to filter both, on local and remote fields, in the same request.
    3 Because replicated data is accessed, the user permission checks of the remote system aren’t evaluated.
    4 Depends on the connectivity and performance of the remote system.

    Connect and Deploy

    Using Destinations

    Destinations contain the necessary information to connect to a remote system. They’re basically an advanced URL, that can carry additional metadata like, for example, the authentication information.

    You can choose to use SAP BTP destinations or application defined destinations.

    Use SAP BTP Destinations

    CAP leverages the destination capabilities of the SAP Cloud SDK.

    Create Destinations on SAP BTP

    Create a destination using one or more of the following options.

    • Register a system in your global account: You can check here how to Register an SAP System in your SAP BTP global account and which systems are supported for registration. Once the system is registered and assigned to your subaccount, you can create a service instance. A destination is automatically created along with the service instance.

    • Connect to an on-premise system: With SAP BTP Cloud Connector, you can create a connection from your cloud application to an on-premise system.

    • Manually create destinations: You can create destinations manually in your SAP BTP subaccount. See section destinations in the SAP BTP documentation.

    • Create a destination to your application: If you need a destination to your application, for example, to call it from a different application, then you can automatically create it in the MTA deployment.

    Use Destinations with Node.js

    In your package.json, a configuration for the API_BUSINESS_PARTNER looks like this:

    "cds": {
        "requires": {
            "API_BUSINESS_PARTNER": {
                "kind": "odata",
                "model": "srv/external/API_BUSINESS_PARTNER",
                "credentials": {
                    "destination": "S4HANA",
                    "path": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
                }
            }
        }
    }
    

    If you’ve imported the external service definition using cds import, an entry for the service in the package.json has been created already. Here you specify the name of the destination in the credentials block.

    In many cases, you also need to specify the path prefix to the service, which is added to the destination’s URL. For services listed on the SAP API Business Hub, you can find the path in the linked service documentation.

    Since you don’t want to use the destination for local testing, but only for production, you can profile it by wrapping it into a [production] block:

    "cds": {
        "requires": {
            "API_BUSINESS_PARTNER": {
                "kind": "odata",
                "model": "srv/external/API_BUSINESS_PARTNER",
                "[production]": {
                    "credentials": {
                        "destination": "S4HANA",
                        "path": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
                    }
                }
            }
        }
    }
    
    Use Destinations with Java

    Destinations are configured in Spring Boot’s application.yaml file:

    cds:
      remote.services:
      - name: API_BUSINESS_PARTNER
        destination:
          name: "cpapp-bupa"
          suffix: "/sap/opu/odata/sap"
          type: "odata-v2"
    

    Learn more about configuring destinations for Java.

    Use Application Defined Destinations

    If you don’t want to use SAP BTP destinations, you can also define destinations in your application configuration.

    For Java, you can use the APIs offered by SAP Cloud SDK to create destinations programmatically.

    Learn more about programmatic destination registration.

    For Node.js, just specify the destination properties in credentials.destinations instead of putting the name of a destination there.

    This is an example of a destination using OAuth2 client credentials:

    "cds": {
        "requires": {
            "REVIEWS": {
                "kind": "odata",
                "model": "srv/external/REVIEWS",
                "[production]": {
                    "credentials": {
                        "url": "https://reviews.ondemand.com/reviews",
                        "authentication": "OAuth2ClientCredentials",
                        "tokenServiceUrl": "https://authentication.ondemand.com/oauth/token",
                        "clientId": "<set from code or env>",
                        "clientSecret": "<set from code or env>",
                        "headers": {
                          "X-Custom-Header": "custom value"
                        }
                    }
                }
            }
        }
    }
    

    Learn more about destination properties.

    You shouldn’t put any sensitive information here.

    Instead, set the properties in the bootstrap code of your CAP application:

    const cds = require("@sap/cds");
    
    if (cds.env.requires?.credentials?.destination?.authentication === "OAuth2ClientCredentials") {
      const credentials = /* read your credentials */
      cds.env.requires.destination.clientId     = credentials.clientId;
      cds.env.requires.destination.clientSecret = credentials.clientSecret;
    }
    

    You might also want to set some values in the application deployment. This can be done using env variables. For this example, the env variable for the URL would be cds_requires_REVIEWS_credentials_destination_url.

    This variable can be parameterized in the manifest.yml for a cf push based deployment:

    applications:
    - name: reviews
      ...
      env:
        cds_requires_REVIEWS_credentials_destination_url: ((reviews_url))
    
    cf push --var reviews_url=https://reviews.ondemand.com/reviews
    

    The same can be done using mtaext file for MTA deployment.

    If the URL of the target service is also part of the MTA deployment, you can automatically receive it as shown in this example:

    mta.yaml:

     - name: reviews
       provides:
        - name: reviews-api
          properties:
            reviews-url: ${default-url}
     - name: bookshop
       requires:
        ...
        - name: reviews-api
       properties:
         cds_requires_REVIEWS_credentials_destination_url: ~{reviews-api/reviews-url}
    

    .env

    cds_requires_REVIEWS_credentials_destination_url=http://localhost:4008/reviews
    

    For the configuration path, you must use the underscore (“_”) character as delimiter. CAP supports dot (“.”) as well, but Cloud Foundry won’t recognize variables using dots. Your service name mustn’t contain underscores.

    Connect to Remote Services from Local

    Run your application in a “hybrid” mode, to test the connection before deployment.

    Use a dedicated profile in your package.json, for example hybrid. Configure the desired configuration in your package.json file, which looks like the [production] profile:

    "cds": {
        "requires": {
            "API_BUSINESS_PARTNER": {
                "kind": "odata",
                "model": "srv/external/API_BUSINESS_PARTNER",
                "[production]": {
                    "credentials": {
                        "destination": "S4HANA",
                        "path": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
                    }
                },
                "[hybrid]": {
                    "credentials": {
                        "destination": "S4HANA",
                        "path": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
                    }
                }
            }
        }
    }
    

    Learn how to connect to the SAP API Business Hub sandbox.

    You need a new profile, because the production profile switches other behavior as well, which isn’t desired here. For example, it switches from sqlite to hana.

    You can use the --profile parameter to select your profile:

    cds watch --profile hybrid
    

    The previous command doesn’t work as-is. You need to provide credentials from Cloud Foundry to connect to the remote services.

    To avoid storing credentials, you can retrieve it from the SAP BTP Cloud Foundry environment without storing them locally. But you need to have (1) your application deployed and be (2) logged in your Cloud Foundry space using the cf command line.

    For example, you can use the following syntax with bash or similar shells:

    VCAP_SERVICES=$(./your-script-returning-the-credentials) cds watch --profile hybrid
    

    If you just want to use the VCAP_SERVICES variable from your running Cloud Foundry application, you can use this command:

    VCAP_SERVICES=$(cf env <CF-APP-NAME> | perl -0pe '/VCAP_SERVICES:(.*?)VCAP_APPLICATION:/smg; $_=$1') cds watch --profile hybrid
    

    Use Locally Stored Credentials

    Another option is to store credentials in a local file.

    However, this has disadvantages:

    1. Credentials are stored on your disk without encryption.
    2. Credentials can leak if you accidentally check them in your source code repository.
    3. They’re always active. You can’t switch them with profiles or with the command line easily.

    You write environment variables as a JSON map in the default-env.json file or with syntax name=value in the .env file of your project.

    Make sure to exclude .env and default-env.json files from your source code management! Add them to .gitignore, for example.

    Never store credentials for productive accounts!

    CAP supports to read credentials from VCAP_SERVICES environment variable, which is a convention from Cloud Foundry. You can inspect running applications to understand its construction.

    Learn more about programmatic destination registration.

    Connect to an Application Using the Same XSUAA (Forward Authorization Token)

    If your application consists of microservices and you use one (or more) as a remote service as described in this guide, you can leverage the same XSUAA instance. In that case, you don’t need an SAP BTP destination at all.

    Assuming that your microservices use the same XSUAA instance, you can just forward the authorization token. The URL of the remote service can be injected into the application in the MTA or CF deployment using application defined destinations.

    Forward Authorization Token with Node.js

    You can add the authorization header for the outbound request using the send API:

      const result = await orders.send(SELECT("Orders").limit(20), headers: { authorization: req.headers.authorization });
    

    If you need to call the service in many occasions, it’s more convenient to implement a small wrapper that does the job for you:

    const ordersOrig = await cds.connect.to("Orders")
    
    const forwardAuthHeader = String(cds.requires.ORDERS?.credentials?.destination?.url).startsWith("https://")
    const orders = forwardAuthHeader ? {
      tx: req => {
        const authorization = req.headers.authorization
        return {
          run: query => ordersOrig.send({
            query, headers: { authorization }
          })
        }
      }
    } : ordersOrig
    

    In that case, you call the remote service just like this:

      const result = await orders.tx(req).send(SELECT("Orders").limit(20));
    

    Deployment

    Your micro service needs bindings to the XSUAA and Destination service to access destinations on SAP BTP. If you want to access an on-premise service using Cloud Connector, then you need a binding to the Connectivity service as well.

    Learn more about deploying CAP applications. Learn more about deploying an application using the end-to-end tutorial.

    Add Required Services to Cloud Foundry Manifest Deployment

    The deployment with Cloud Foundry manifest is described in the deployment guide. You can follow this guide and make some additional adjustments to the generated services-manifest.yml and the services.yml files.

    Add XSUAA, Destination, and Connectivity service to your services-manifest.yml file.

      - name: cpapp-uaa
        broker: xsuaa
        plan: application
        parameters: xs-security.json
        updateService: true
    
      - name: cpapp-destination
        broker: destination
        plan: lite
        updateService: false
    
      # Required for on-premise connectivity only
      - name: cpapp-connectivity
        broker: connectivity
        plan: lite
        updateService: false
    

    Add the services to your microservice’s services list in the manifest.yml file:

    - name: cpapp-srv
      services:
      - ...
      - cpapp-uaa
      - cpapp-destination
      # Required for on-premise connectivity only
      - cpapp-connectivity
    

    Push the application.

    cf create-service-push  # or `cf cspush` in short from 1.3.2 onwards
    

    Add Required Services to MTA Deployments

    The MTA-based deployment is described in the deployment guide. You can follow this guide and make some additional adjustments to the generated mta.yml file.

    Add XSUAA, Destination, and Connectivity service to your mta.yaml file:

    - name: cpapp-uaa
      type: org.cloudfoundry.managed-service
      parameters:
        service: xsuaa
        service-plan: application
        path: ./xs-security.json
    
    - name: cpapp-destination
      type: org.cloudfoundry.managed-service
      parameters:
        service: destination
        service-plan: lite
    
    # Required for on-premise connectivity only
    - name: cpapp-connectivity
      type: org.cloudfoundry.managed-service
      parameters:
        service: connectivity
        service-plan: lite
    

    Add the services as requirement for your microservice in your mta.yaml file:

    - name: cpapp-srv
      ...
      requires:
        ...
        - name: cpapp-uaa
        - name: cpapp-destination
          # Required for on-premise connectivity only
        - name: cpapp-connectivity
    

    Build and deploy your application:

    # build .mtar
    mbt build -t ./
    
    # deploy
    cf deploy <.mtar file>  # for example, bookshop_1.0.0.mtar
    

    Destinations and Multitenancy

    With the destination service, you can access destinations in your provider account, the account your application is running in, and destinations in the subscriber accounts of your multitenant-aware application.

    Use Destinations from Subscriber Account

    Customers want to see business partners from, for example, their SAP S/4 HANA system.

    As provider, you need to define a name for a destination, which enables access to systems of the subscriber of your application. In addition, your multitenant application or service needs to have a dependency to the destination service.

    The subscriber needs to create a destination with that name in their subscriber account, for example, pointing to their SAP S/4HANA system.

    Destination Resolution

    Destinations are looked up using the following rules:

    Runtime Rules
    Java The destination is read from the tenant of the request’s JWT (authorization) token.
    If no JWT token is present, the destination is read from the tenant of the application’s XSUAA binding.
    Node.js The destination is read from the tenant of the request’s JWT (authorization) token.
    If no JWT token is present or the destination isn’t found, the destination is read from the tenant of the application’s XSUAA binding.

    ❗ JWT token vs. XSUAA binding
    Using the tenant of the request’s JWT token means reading from the subscriber subaccount for a multitenant application. The tenant of the application’s XSUAA binding points to the destination of the provider subaccount, the account where the application is deployed to.

    Currently, it isn’t possible to configure a different destination lookup behavior in CAP.

    For a multitenant application, it isn’t possible to ensure that a destination is always read from the provider account. It is recommended to manually create the destination if this is required.

    Add Qualities

    Resilience

    There are two ways to make your outbound communications resilient:

    1. Run your application in a service mesh (for example, Istio, Linkerd, etc.).
    2. Implement resilience in your application.

    Refer to the documentation for the service mesh of your choice for instructions. No code changes should be required.

    To build resilience into your application, there are libraries to help you implement functions, like doing retries, circuit breakers or implementing fallbacks.

    Resilience in Java

    You can use the resilience features provided by the SAP Cloud SDK with CAP Java. You need to wrap your remote calls with a call of ResilienceDecorator.executeSupplier and a resilience configuration (ResilienceConfiguration). Additionally, you can provide a fallback function.

    ResilienceConfiguration config = ResilienceConfiguration.of(AdminServiceAddressHandler.class)
      .timeLimiterConfiguration(TimeLimiterConfiguration.of(Duration.ofSeconds(10)));
    
    context.setResult(ResilienceDecorator.executeSupplier(() ->  {
      // ..to access the S/4 system in a resilient way..
      logger.info("Delegating GET Addresses to S/4 service");
      return bupa.run(select);
    }, config, (t) -> {
      // ..falling back to the already replicated addresses in our own database
      logger.warn("Falling back to already replicated Addresses");
      return db.run(select);
    }));
    

    See the full example

    Resilience in Node.js

    There’s no resilience library provided out of the box for CAP Node.js. However, you can use packages provided by the Node.js community. Usually, they provide a function to wrap your code that adds the resilience logic.

    Tracing

    CAP adds headers for request correlation to its outbound requests that allows logging and tracing across micro services.

    Learn more about request correlation in Node.js. Learn more about request correlation in Java.

    Feature Details

    Querying API Features

    Feature Java Node.js
    READ supported supported
    INSERT/UPDATE/DELETE supported supported
    Actions supported supported
    columns supported supported
    where supported supported
    orderby supported supported
    limit (top & skip) supported supported
    $apply (groupedby, …) not supported not supported
    $search (OData v4) supported supported
    search (SAP OData v2 extension) supported supported

    Supported Projection Features

    Feature Java Node.js
    Resolve projections to remote services supported supported
    Resolve multiple levels of projections to remote services supported supported
    Aliases for fields supported supported
    excluding supported supported
    Resolve associations (within the same remote service) supported supported
    Redirected associations supported supported
    Flatten associations not supported not supported
    where conditions not supported not supported
    order by not supported not supported
    Infix filter for associations not supported not supported
    Model Associations with mixins supported supported
    Show/Hide Beta Features