Skip to Content
Technical Articles
Author's profile photo Wouter Lemaire

Use SCP CF Destination service in NodeJs locally

Introduction

Recently, I was working on a NodeJS application to run on SAP Cloud Platform Cloud Foundry. In this application I had to read data from another service, do some stuff with the data and expose it again as a simple web service. On top of the service, I created a UI5 app that consumes the service to visualize the data.

 

You could wonder why I want to put a NodeJS app in between. I do this for the following reasons:

  • The data comes from an API from another system and some parts should be protected from the end user
  • I also need to apply some manipulation before showing the data, I put this logic in NodeJS so I keep the UI simple and clean
  • This gives me full control of what the end user can access via the API that I provide and not the full API of the system behind it

 

Problem

For accessing the other external service, I used the destination service. This service helps you to store all your connections in a central place in the SAP Cloud Platform. It allows you to configure different environments (DEV,ACC,PRD,…) for different SCP accounts but also to change the configuration without touching your code. 

 

There is a lot of documentation and blogs on how to use this Destination service in different languages, how it’s being used with the approuter, how to use it in UI5, in CAP, in NodeJS … But for some reason, I was not able to find any documentation on how to test your NodeJS app with a destination locally. It could be that I didn’t look at the right place or overlooked something. In the end, I found the solution on some GitHub project by accident… I want to save you the time to search and share my solution.

 

To explain the problem, I’m going to create a new NodeJS app. I will take you through each step from the beginning.

 

Start by opening the command line interface in the folder of your project. Create a new NodeJS app by running “npm init” in the CLI

npm init

Answer all the questions:

We need to install the following libraries for building an API in NodeJS and to call an other/externel service:

  • Express
    • This will make the app act as an API
  • Request & request-promise
    • Used for calling the other API

Run the following command:

npm i express request request-promise

We also need to install sap-xsenv library from SAP. This will be used to use the Destination service.

Run the following command:

npm i @sap/xsenv

Add a start script to the package.json. This will be executed by CloudFoundry when the app is deployed to start the app. In the script I run my javascript file in NodeJS:

"start": "node index"

 

The package. json file should look like this if you followed all the steps correctly:

At this point, the start script won’t work. We need to create index.js and add the following code:

⇒ This code will get the data from another service and expose it via a NodeJS Express service by using the SCP CF destination service. 

⇒ I’m not going to explain the code in detail. It comes from the example of Marius Obert’s blog where he already explains the code: https://blogs.sap.com/2018/10/08/using-the-destination-service-in-the-cloud-foundry-environment/

 

const express = require('express');
const rp = require('request-promise');
const xsenv = require('@sap/xsenv');
 
const app = express()
const port = process.env.PORT || 3000;
const dest_service = xsenv.getServices({ dest: { tag: 'destination' } }).dest;
const uaa_service = xsenv.getServices({ uaa: { tag: 'xsuaa' } }).uaa;
const sUaaCredentials = dest_service.clientid + ':' + dest_service.clientsecret;
const sDestinationName = 'My-Destination';
 
 
app.get('/data', (req, res) => {
    return rp({
        uri: uaa_service.url + '/oauth/token',
        method: 'POST',
        headers: {
            'Authorization': 'Basic ' + Buffer.from(sUaaCredentials).toString('base64'),
            'Content-type': 'application/x-www-form-urlencoded'
        },
        form: {
            'client_id': dest_service.clientid,
            'grant_type': 'client_credentials'
        }
    }).then((data) => {
        const token = JSON.parse(data).access_token;
        return rp({
            uri: dest_service.uri + '/destination-configuration/v1/destinations/' + sDestinationName,
            headers: {
                'Authorization': 'Bearer ' + token
            }
        });
    }).then((data) => {
        const oDestination = JSON.parse(data);
        const token = oDestination.authTokens[0];
        return rp({
            method: 'GET',
            uri: oDestination.destinationConfiguration.URL + "/MyOtherService",
            headers: {
                'Authorization': `${token.type} ${token.value}`
            }
        });
    }).then((result) => {
        res.send(result);
    }).catch((error) => {
        res.send("error: " + JSON.stringify(error));
    });
});
 
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

 

This works perfectly fine on SCP CF. But when you run it locally, it is not able to find your destination:

Solution

As mentioned earlier in this blog, it is not a big thing to solve this problem. But as I was not able to find a well documented solution easily I think it’s still worth to share how to solve this.

 

Just as a side note, you are maybe thinking that this could be solved by using the approuter. In this case I want to use the destination service in my NodeJS app and not expose the external service. Defining a route in the configuration of the approuter will expose the complete external service.

 

Follow the next steps to solve this problem:

1. Create a destination to your external service  

Create your destination

2. Create a destination instance

a. You can do this via the SCP Cockpit

Go to your space -> service marketplace 

Select instances

Click on new instance and follow the wizard

b. Or by using the command:

cf create-service destination lite my-destination-service

Using the SCP cockpit or the CF CLI will both create the destination service which will be visible in the service instances of the destination dervice:

3. Get the VCAP details of the destination instance

a. In SCP Cockpit (starting from the destination instance that you created in the previous step)

b. via Command line

cf create-service-key my-destination-service my-destination-service-key

cf service-key my-destination-service my-destination-service-key

4. Do the same for the XSUAA service

cf create-service xsuaa application my-uaa-service

cf create-service-key my-uaa-service my-uaa-service-key

cf service-key my-uaa-service my-uaa-service-key

5. Create a file default-env.json and add the VCAP details in it. Fill in the service keys of each service in the “credentials” section. This will be used to get to all the details of the used services when running the application locally.

{
    "VCAP_SERVICES": {
        "xsuaa": [
            {
                "name": "my-uaa-service",
                "label": "xsuaa",
                "tags": [
                    "xsuaa"
                ],
                "credentials": <my-xsuaa-service-key>
            }
        ],
        "destination": [
            {
                "name": "my-destination-service",
                "label": "destination",
                "tags": [
                    "destination"
                ],
                "credentials":<my-destination-service-key>
            }
        ]
    }
}

 

6. The file only is not enough. It needs to be loaded into the NodeJS app locally and use the bounded services when running in CF. Add the following line in your code to do this:

 

xsenv.loadEnv();

Run “node index” to test again:

Now you can test your app locally! 🙂

 

You probably also want to know if the apps works in CloudFoundry. Before deploying this, you need to add a manifest.yaml file and define the used services to it. In our example, we use the xsuaa and the destination service. The xsuaa is used to get access to the destination service.

 

Create manifest.yaml like this:

Now the app can be deployed to CF with the following command:

cf push dest-demo-nodejs –random-route

 

Result in SCP:

Full project: https://github.com/lemaiwo/SCP-CF-NodeJS-Example

 

 

You could also consider doing this by using the Cloud Application Programming model. With CAP you will be able to define the structure of your API as an OData V4 service.

 

Example where I do exactly the same with CAP:

https://github.com/lemaiwo/SCP-CF-CAP-NodeJS-Example

 

When using CAP to do this will still requires the config default-env.json. The big difference when using CAP, is that you do not need to use the “loadEnv” function. This is being handled by the CAP framework.

Not needed: 

xsenv.loadEnv();

 

In the example with CAP, I also used another solution for calling the external API. I used the CF Axios library which was created by a former colleague of mine. Big thanks to Joachim Van Praet for this one!

https://blogs.sap.com/2019/11/13/consuming-destinations-in-cloud-foundry-using-axios-in-a-nodejs-application/

 

If you want to take it to the next step and setup authentication, you can follow this:

https://cap.cloud.sap/docs/node.js/authentication

 

You can also use MTA for deploying to SCP CF, follow these steps: https://cap.cloud.sap/docs/advanced/deploy-to-cloud

Assigned Tags

      17 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Jorge Sousa Villafaina
      Jorge Sousa Villafaina

      Many thanks! Very useful for me. For using the destination into an UI5 app in additional HTML5 module the steps of "Solution" would be the same, doesn't it?

      Author's profile photo Wouter Lemaire
      Wouter Lemaire
      Blog Post Author

      Yes, but in that case you will also need the approuter

      Author's profile photo Jhodel Cailan
      Jhodel Cailan

      Hi Wouter,

      We are both looking for the same or similar thing! Is your external service an OData service? If that's the case, you might want to look at my 2 part series blog -- https://blogs.sap.com/2020/05/26/cap-consume-external-service-part-1/ where I relied purely on CAP Model.

      Thanks and regards,

      Jhodel

      Author's profile photo Wouter Lemaire
      Wouter Lemaire
      Blog Post Author

      Thanks, I was using the SCIM API of IAS which has no edmx file...  It is a REST service but not a fully compatible OData service. I looked at your solution but I was not able to use it.

      Author's profile photo Jhodel Cailan
      Jhodel Cailan

      I knew it! I asked the CAP development team about non-OData support but sadly it’s not supported yet.  We just need to go through manual process like the one you outlined here. Thanks for your detailed blog btw!

      Author's profile photo Deepak Sahu
      Deepak Sahu

      Hi Wouter,

      Awesome blog. Thanks for this.

      I am trying out a similar scenario and have cloned your project to extend but stuck with an issue where I am getting the below error when the run the app locally. I followed Jodel Cailan's Blog on consuming the service using app router and the same destination works and returns back the service but the scenario is to make this call to the service from inside the node js function.

      https://github.com/lemaiwo/SCP-CF-CAP-NodeJS-Example

      I have put up the question here for the same.

       

      Please see the scenario details here.

      1. The OData service I am trying to consume is a HANA XSOData service in Neo Platform. The destination to the neo HANA DB is configured in CF sub-account
      2. I have also created a destination and UAA service and updated the default.env with the service keys
      3. Have also created the dependencies in the mta.yaml file
        ## Generated mta.yaml based on template version 0.2.0
        ## appName = destination-demo
        ## language=nodejs; multiTenant=false
        ## approuter=
        _schema-version: '3.1'
        ID: destination-demo
        version: 1.0.0
        description: "Demo SCP CF destination local"
        parameters:
          enable-parallel-deployments: true
          
          
        build-parameters:
          before-all:
           - builder: custom
             commands:
              - npm install
              - npx cds build
        
        modules:
         # --------------------- SERVER MODULE ------------------------
         - name: destination-demo-srv
         # ------------------------------------------------------------
           type: nodejs
           path: gen/srv
           parameters:
             memory: 128M
             disk-quota: 128M
           properties:
             EXIT: 1  # required by deploy.js task to terminate
           requires:
              # Resources extracted from CAP configuration
              - name: demo-destination
              - name: demo-uaa   
        resources:
          - name: demo-uaa
            type: org.cloudfoundry.managed-service
            parameters:
              path: ./xs-security.json
              service-plan: application
              service: xsuaa
        
          - name: demo-destination
            type: org.cloudfoundry.existing-service
        
        
        
        ​
      4. Below is the node js code
        const SapCfAxios = require('sap-cf-axios').default;
        const destination = SapCfAxios('CF_Test');
        
        module.exports = (srv) => {
            srv.on('CatalogService.getAPICall', async (req) => {
                let response = await destination({
                    method: 'GET',
                    url: "/CF_Test/TestOService",
                    headers: {
                        "content-type": "application/json"
                    }
                });
                return JSON.stringify(response.data)
                /*return response.data.Resources.map((user) => ({
                    id: user.id,
                    userName: user.userName,
                    name: user.displayName,
                    addresses: user.addresses,
                    emails: user.emails.map((mail) => { return { value: mail.value } })
                }));*/
            })
        }​

      Would really appreciate if you could provide any guidance on this.

      Author's profile photo Wouter Lemaire
      Wouter Lemaire
      Blog Post Author

      I recently had similar problems with the Axios library. Have you tried it this way: https://github.com/lemaiwo/SCP-CF-NodeJS-Example/blob/master/index.js ? Doing the steps myself helped for me.

       

      Author's profile photo Deepak Sahu
      Deepak Sahu

      Thanks Wouter,

      That code example worked perfectly. I was also trying to test a connection setup to my on-premise ERP system using the cloud connector. Could you please point me to any direction if you have done the same?

      Appreciate your help with all this 🙂

      Best Regards,

      Deepak

      Author's profile photo Matthew Reddy
      Matthew Reddy

      Hi Deepak – I am also having an issue testing locally with the cloud connector/on-prem scenario. Presumably it is because our local networks can’t resolve the <hostname>:<port> of the cloud connector virtual host.

      I was planning on trying something with using default-env.json to create fake destinations that point to the system directly (for instance, if i log onto the vpn) but wanted to double check if there were any good solutions already out there.

      Did you end up figuring something out?

      Author's profile photo Rajdeep BHUVA
      Rajdeep BHUVA

      Hi Wouter,

       

      Awesome Blog post!

      I want to use destination service in my local project which has node.js module and SAPUI5 module. I Created service key in destination service and using it i created user provided service with tag destination. It is working fine for Node modules but, for sapui5 module it is using approuter and somehow approuter is not fetching the details from user provided service.

      Do you know anything we could do here which can help approuter to use destination service using user provided service?

       

       

      Thanks,

      Rajdeep Bhuva

      Author's profile photo Artem Kovalov
      Artem Kovalov

      Hi Rajdeepkumar Bhuva ,

       

      Try using the official SAP Cloud SDK for Javascript https://sap.github.io/cloud-sdk/docs/js/features/connectivity/destination-js-sdk. It has extended functionality, supported by SAP and it's mostly Open Source https://github.com/SAP/cloud-sdk besides type-safe client, aka VDM https://www.npmjs.com/search?q=%40sap%2Fcloud-sdk-vdm-*

       

      Best,

      Artem

      Author's profile photo Artem Kovalov
      Artem Kovalov

      Hi Wouter Lemaire ,

      Have you considered an official SAP Cloud SDK library for your case? It has abstractions for Destinations and Proxies handling similar to the library you're referring to. You can also check out our Github repo: https://github.com/SAP/cloud-sdk.

      Additionally, we provide a generator for OData services as well as a set of pre-generated libraries of S/4HANA Cloud and some other SAP services.

      Best,

      Artem

      Author's profile photo Matthew Reddy
      Matthew Reddy

      Hey Wouter - great stuff as usual! Appreciate your contributions to the community

      Author's profile photo Vidhya K Pai
      Vidhya K Pai

      Hi Wouter,

      I’m trying to import “@sap/xsenv” library to the Component.js file of my UI5 project to read a User-provided variable that is available in the cf env of the application I'm working with. I have tried the below methods of doing this and none of them have worked:
      1. var xsenv = sap.ui.require(“@sap/xsenv”);
      2. var xsenv = require(‘@sap/xsenv’);
      3. process.env(‘SERVICE_NAME’)
      The first one results in xsenv variable having undefined values. The second and the third ones say that the require and process are not defined. I tried to import in the sap.ui.define too. That did not work either.
      Is there a way to achieve this?

      Kind Regards,

      Vidhya
      Author's profile photo Jakob Ruf
      Jakob Ruf

      Hi Vidhya,

      I think the problem here is that you are trying to require a NodeJS module in a non-Node application (SAPUI5 app). If you want to access data that is provided by the @sap/xsenv module, then you'll probably have to implement a service in your CF environment that can access the xsenv properties and return them to the UI5 app

      Kind regards

      Jakob

      Author's profile photo Andreas Gall
      Andreas Gall

      Wouter Lemaire thanks for your great blog, especially your link to https://github.com/lemaiwo/SCP-CF-CAP-NodeJS-Example was really helpful for my use case: had to post data within a CAP app to a rest API that requires an X-CSRF token and an x-www-form-urlencoded. Unfortunately CAP doesn't offer there any flexibility.

      Regards, Andreas

      Author's profile photo Eshan Gandhi
      Eshan Gandhi

      Hello Wouter,

      I am getting this error.