Skip to Content
Technical Articles
Author's profile photo Carlos Roggan

SAP Cloud Platform Backend service: Tutorial [18]: API: called: from: external: node.js

This blog is part of a series of tutorials explaining the usage of SAP Cloud Platform Backend service in detail.

Quicklinks:
Code

You can use Backend service to easily creat…..
Oh, no, thanks, I have enough APIs, guess I’ll search for another tutorial seri…
Hey, stop, just tell what you’re looking for
Guidance how to write node.js app to call APIs from exter…
But that’s what I was about to to say…
Ah, cool, just say

Assuming you have created enough APIs in the Backend service cockpit
Assuming you have learned how to access and test your APIs from external REST client
Maybe now you wish to take a next step and programmatically call the API from outside the cloud.
Could be necessary e.g. for automatically testing your APIS with local test script.
Such script might be running in a Jenkins build server, whatever.
And I wish example code
A wish which I fully understand
And also I wish to support such wish.
So here it comes:
Example node.js code to call Backend service API

Note:
I think that you should be able to follow this blog, even if you’re completely new to node.js

Prerequisites

Although in this tutorial we just write a small basic node.js application, there are quite a few prerequisites and configuration steps to be done.

Cloud:
You’ve created an API in Backend service. (see here)
You’ve gone through the description of manually calling API from REST client tool
In order to achieve that, there’s a prerequisite: configure security in Cloud account
You might also go through the OAuth 2 blog to understand a bit more about the background

node.js:
You might want to have a look at this section, where I’ve already described the necessary steps to be prepared to run node
Note:
In this present blog, no SAP-specific node modules are required, so you don’t need to configure the npm registry

Prepare it

Before writing the code, it makes sense to collect some user-specific data to be entered later:

Backend service API

You need the URL of the OData service which you want to call.
Important to mention:
To view the URL, we usually open the URL in the cockpit of Backend service.
But this URL is not really the one which we can use to call it externally.
So we need to slightly modify the URL:
It has to be prefixed with backend-service-api instead of
<yourSubaccount>-backend-service
See my example below

Cloud environment

Assuming, you have prepared your cloud account with creating a service instance of xsuaa and creating a service key
Now you can go there and take a note of the required properties
Furthermore, take a note of your OData service which you want to call, i.e. the API created on Backend service

These are my example values:

API https://backend-service-api.<…>/odatav2/DEFAULT/PRODUCTSERVICE;v=1/Products
clientid
sb-xsuaaName!t12345
secret
VZz5this37dJ6eTVmightbeKhfGf56lw=
Uri https://<subaccount>.authentication.eu10.hana.ondemand.com/oauth/token
user myDummyUser@myMail.com
pwd theUsual123

Local environment

Prepare your local environment for node.js development:
Experienced users may skip this section

All we need is a dedicated folder
Create a folder
e.g. C:\tmp_bsnode
Open command shell and navigate to that folder

Now we can install the required dependencies.
Run the following command:

npm install client-oauth2

like here:

This command installs a node module which helps us dealing with the OAuth2 flow
Similarly execute the following 2 commands

npm install request

and

npm install request-promise

Result:
In our file system, the folder node_modules has been created in our working directory.
It contains all required dependencies

Note:
I think it is easier for the moment to live without package.json file
Reason is that it isn’t possible to attach files or archives to the blog.
So I think it is easier to follow the blog, if there’s only one file

Code it

Now it is time for our application code.
Inside our working folder, we create a javascript file
e.g. app.js

Note:
Before you go ahead:
The next step is to paste the code and afterwards to run it BUT:
BUT there’s a BUT:
Don’t forget it:
B U T before running the app, you need to read a NOTE.
Don’t forget it
Otherwise it won’t work.
And you’ll complain.
BUT: I’ve warned you early enough

OK
OK, open the file with an editor of your choice, e.g. Notepad

Note:
A good choice is Visual Studio Code
Yes, I installed it when following the CDS modelling blog
Fantastic

Paste the following code:

'use strict';

const oauthClient = require('client-oauth2');
const request = require('request-promise');

// OData service, created in Backend service
const SERVICE_URL = 'https://backend-service-api.cfapps.eu10.hana.ondemand.com/odatav2/DEFAULT/PRODUCTSERVICE;v=1/Products';

// helper method which uses client-oauth2 library to get access token from XSUAA 
const _getAccessToken = function() {
    return new Promise((resolve, reject) => {
        const oautClient = new oauthClient({
            accessTokenUri: 'https://bssubaccount.authentication.eu10.hana.ondemand.com/oauth/token',
            clientId: 'sb-XsuaaForAuthCode!t13020',
            clientSecret: 'VZz539437dJ6eTVmigKhfGG89lw=',
            scopes: []
        });

        oautClient.owner.getToken('yourUser@yourMail.com', 'theUsual123')
        .then(result => {
            resolve({accessToken: result.accessToken});
        })
        .catch(error => {
            reject({message: 'Error: failed to get access token.', error: error}); 
        });
    });
}

 // helper method for calling Backend service API, using the previously fetched token
 const _doQUERY = function (serviceUrl, accessToken){
    return new Promise (function(resolve, reject){
        const options = {
            url: serviceUrl,
            resolveWithFullResponse: true ,
            headers: { 
                Authorization: 'Bearer ' + accessToken, 
                Accept : 'application/json'
            }
        };
        
        request(options)
        .then(response => {
            if(response && response.statusCode == 200){
                resolve({responseBody: response.body});
            }
            reject({ message: 'Error while calling OData service'});
        })  
        .catch(error => {
            reject({ message: 'Error occurred while calling OData service', error: error });
        });
    });
 };

// The script calling Backend service API
console.log('Calling Backend service-API ...');
_getAccessToken()
.then(result => {
    console.log('Successfully fetched OAuth access token: ' +  result.accessToken.substring(0,16));
    return _doQUERY(SERVICE_URL, result.accessToken);
})
.then(result => {
    console.log('Successfully called OData service. Response body: ' + result.responseBody.substring(0,111));
    // do anything like:  JSON.parse(result.responseBody).d.results;
})
.catch(error => {
    console.log(error.message + ' Reason: ' + error.error);
});

Node:
Now it comes, the node…
What?

Note:
Sorry, a typo, I don’t mean a node, I mean a note

The Note:
You have to change the code:
Make sure to enter your personal data, which you prepared in the preparation section above
Line 7: Enter the URL of your personal Backend service API, it should be a collection
Line 13: Enter the URL which comes from your xsuaa instance, it should end with /oauth/token
Line 14: Enter the clientid of your xsuaa instance
Line 15: Enter the clientsecret of your xsuaa instance
Line 19: Enter your personal cloud user
Line 19: Enter the password of your personal cloud user

Note:
That’s it, now you can run it

Run it

To run the node.js application, change to your command prompt and make sure you’re in the same working directory as usual
Type the following command and press enter

node app.js

As a result, the application is executed.
It prints a few success messages.
What success, what messages?
No?
It prints an error message: Error: failed to get access token ???
I cannot believe it….Did you read my note…
Which note?
aaaaarghhh…
See here how to adapt the sample code:

OK
After adapting the code to your personal data, you’ll get the 2 success messages.
Hopefully…

Note:
The following section explains a little bit what we’ve been doing

Understand it

Note:
First of all, my apologies for the code which probably could be better, but I’m not a pro and I’ve tried to keep it short
Yes, it looks awful
OK
What is done in that ugly code?

The application does 2 things
1. use the node module client-oauth2 to do the OAuth 2 flow in order to get the access token
2. use the node module request-promise to call  the OData service (with token)

Both are encapsulated in helper methods (line 10 and line 29)
Both helper methods are called subsequently (with Promises), starting in the “Script” section at line 54

Note:
For calling OData services, there are other node packages, but today I wanted to be simple.

What is interesting in the code?
We use the “Resource Owner Password Credentials” grant type of OAuth 2, therefore we use the “owner” to fetch the token.

oautClient.owner.getToken('user', 'pwd')

Like that, it is quite easy to get the required token
Once we have it, we use it as header in the request which we send to the OData service:

Authorization: 'Bearer ' + accessToken,

We’re also setting another header:

Accept : 'application/json'

This is done to ensure that the OData service sends its response payload in JSON format.
Like that, it is easy to process further

However, today we don’t process it further…
Oh, already the end?
Yes,but I’ll be back
Hasta la vista Baby

Troubleshooting

What to do if you happily deploy everything and try testing and get….. Forbidden ???
See this blog for a Troubleshooting guide

Summary

In this blog we’ve learned how to call a Backend service API from an external node application.
Honestly, there’s nothing special in this blog.
We’ve

used 2 standard node modules to do 2 standard calls:
Get OAuth token
Call OData service

However, I believe it is always good to have sample code, to be ready to easily use Backend service:
With SAP Cloud Platform Backend service you can easily create APIs and with the help of this blog you can easily consume it with external node.js application

You’ve made it

Glad that I’ve finally concluded my sentence

Advertisements

Little node.js tutorial series:
Call API from external node application: this blog
Call API from node app, deployed to Cloud Foundry, everything done manually
Call API from node app inside CF using XSUAA service binding
Call API from node app inside CF using XSUAA login screen
Call API from node app inside CF using Destination service binding

Call API from external app, means local REST client

Preparation info:
Configure xsuaa for usage from external app/tool
Configure BETA subaccount to enable xsuaa configuration
OAuth intro:
Explaining OAuth 2 mechanism for Backend service, security mechanism
Explaining OAuth “Authorization Code” grant type, also supported by Backend service

All Blogs and tutorials: overview of blog series

Links

https://nodejs.org/en/

client-oauth2

request-promise

 

Assigned Tags

      9 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Mahesh Palavalli
      Mahesh Palavalli

      Hello Carlos Roggan ,

      One more issue I am facing while passing the token and accessing the service, I get this below response.

      <html>
      <head>
      <link rel="shortcut icon" href="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /><script>
      document.cookie="fragmentAfterLogin="+encodeURIComponent(location.hash)+";
      path=/";
      location="https://testola.authentication.eu10.hana.ondemand.com/oauth/authorize?response_type=code&client_id=sb-Backend-service!t6131&redirect_uri=https%3A%2F%2Ftestola-backend-service.cfapps.eu10.hana.ondemand.com%2Flogin%2Fcallback"
      </script>
      </head>
      </html>

      It is happening both from rest client(postman) and from the node as well. It works till the token is fetched after that step the above response will come.

      BR,
      Mahesh

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hi Mahesh Kumar Palavalli

      I'll check tomorrow, but from a first look, it seems that the xsuaa wants to display the login-form, I can see "response_type=code
      But in this tutorial we're using password-credentials ("owner")
      Maybe you're using wrong oauth-client-method?

      Cheers,

      Carlos

      Author's profile photo Mahesh Palavalli
      Mahesh Palavalli

      Hi Carlos Roggan ,

      I used password credentials to get the token and passed it to the odata request. But again I copied all the node js app code then also it was working till the token after that the same response.

      BR,

      Mahesh

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hi Mahesh Kumar Palavalli

      I’ve gone through the blog myself and for me it works fine.
      In your case the token-fetch works well, so it seems that all private settings, like user-credentials, client-credentials, seem to be correct

      Then, when calling the OData service, it seems that the Backend service tries to display the login screen.
      Means that the token is not good enough…. But please don’t try to improve it by adding some more characters…Backend service is not that easy to satisfy…;-)

      The only ideas I have would be:
      Is the service URL correct? I mean, does it start like this:
      https://backend-service-api.cfapps
      ?
      If you try calling the https://<acc>-backend-service.cfapps as displayed in cockpit, then a session is required and the tool tries to log you in
      Other possible problem: is the JWT token correct? Have you looked into it to check if it contains the required scope and user etc?
      See here: https://blogs.sap.com/2019/07/01/sap-cloud-platform-backend-service-tutorial-29-shooting-error-forbidden/

      Hope to see you smiling soon….! ?
      Cheers,
      Carlos

       

      Author's profile photo Mahesh Palavalli
      Mahesh Palavalli

      😀 😀

      I feel very dumb now, not sure how I missed your point there in blog , probably because of the humor running there 😀 😛

      I needed to change the API URL and which brings me an another question which I will post there. 🙂 🙂

      Thank you so much Carlos Roggan for taking your time to help me out.

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hi Mahesh Kumar Palavalli ,
      don't worry, you're welcome, I'm just glad that it is working now.
      Pls let me know if there's too much Humor, if it confuses too much.
      Anyways, I'll add a note above wrt the URL

      Kind Regards,
      Carlos

      Author's profile photo Mahesh Palavalli
      Mahesh Palavalli

      Hi  Carlos Roggan,

      I was just kidding 😀 and It's perfect and unique 🙂 🙂 So keep them coming 🙂

      Regards,
      Mahesh

      Author's profile photo Badhusha Akhthaar
      Badhusha Akhthaar

      Hi Carlos Roggan ,

       

      I did as you explained in this blog, I am able to get the results . i.e GET method is working.  So the problem is when I try to do the POST from the same node.js code, it throws error. Did some changes in the code.

      It is throws error like

       

      I will add the code aslo.

      'use strict';
      
      const oauthClient = require('client-oauth2');
      const request = require('request-promise');
      
      // OData service, created in Backend service
      const SERVICE_URL = 'https://backend-service-api.cfapps.eu10.hana.ondemand.com/odatav2/DEFAULT/XXXXX_XXXXX;v=1/GeoServiceEntity';
      
      // helper method which uses client-oauth2 library to get access token from XSUAA 
      const _getAccessToken = function() {
          return new Promise((resolve, reject) => {
              const oautClient = new oauthClient({
                  accessTokenUri: 'https://xxxxx.authentication.eu10.hana.ondemand.com/oauth/token',
                  clientId: 'sb-xxxxxTrackapp!t39406',
                  clientSecret: 'uazRF9nCuJxh++RrY32IFLYte4A=',
                  scopes: []
              });
      
              oautClient.owner.getToken('myemail@domain.com', 'myPassword')
              .then(result => {
                  resolve({accessToken: result.accessToken});
              })
              .catch(error => {
                  reject({message: 'Error: failed to get access token.', error: error}); 
              });
          });
      }
      
       // helper method for calling Backend service API, using the previously fetched token
       const _doQUERY = function (serviceUrl, accessToken){
          return new Promise (function(resolve, reject){
              const options = {
                  url: serviceUrl,
                  resolveWithFullResponse: true ,
                  method : 'POST',
                  headers: { 
                      Authorization: 'Bearer ' + accessToken, 
                      Accept : 'application/json'
                  }
              };
              
              request(options,{ 
                  json : {
                      "frieghtorder_id": 50002,
                      "truckid": "string",
                      "start_lat": "123.3456",
                      "start_long": "123.3456",
                      "dest_lat": "123.3456",
                      "dest_long": "123.3456",
                      "current_lat": "123.3456",
                      "current_long": "123.3456"
                    }
              })
              .then(response => {
                  if(response && response.statusCode == 200){
                      resolve({responseBody: response.body});
                  }
                  reject({ message: 'Error while calling OData service'});
              })  
              .catch(error => {
                  reject({ message: 'Error occurred while calling OData service', error: error });
              });
          });
       };
      
      // The script calling Backend service API
      console.log('Calling Backend service-API ...');
      _getAccessToken()
      .then(result => {
          console.log('Successfully fetched OAuth access token: ' +  result.accessToken.substring(0,16));
          return _doQUERY(SERVICE_URL, result.accessToken);
      })
      .then(result => {
          console.log('Successfully called OData service. Response body: ' + result.responseBody);
          // do anything like:  JSON.parse(result.responseBody).d.results;
      })
      .catch(error => {
          console.log(error.message + ' Reason: ' + error.error);
      });

       

      Best Regards,

      Badhusha.

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hello Akhthaar Ali Badhusha , sorry for the late reply, I've seen your comment only now.
      I'm not sure what might be the problem.
      The error message seems to have an issue with the URL, you might want to check if the url you're passing is really sent. Error message might be misleading.
      Another idea, not sure, have you tried to specify the header "content-type"? It should indicate that you're sending json in your payload
      Last idea: usually, OData server requires x-csrf-token to be sent along with modifying request (POST; PUT, DELETE) I'm not sure how Backendservice is behaving in that respect, currently.
      Have you tried the POST request with postman? You can reuse the JWT token that you get in the node app
      Hope that helps,
      Cheers,
      Carlos