Skip to Content
Technical Articles

SAP Cloud Platform Backend service: Tutorial [20]: API: called: from: internal: node.js: with binding

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

Quicklinks:
Code
Reference

In the previous tutorial, we learnt how to deploy a node.js app to Cloud Foundry.
The app did its best to do the best: call a Backend service API.
Today we take the next step…
Again a next step?
Yes, another next step

Because our last app handled the security mechanism in the same way like required for external applications:
We created an XSUAA service instance, created service key, then copy and pasted the params from service key into the code.
You must have felt it: hard-coding the params cannot be the final solution…
. . .
Did you?
What?
Did you feel that hard-coding the params could not be the…
I only felt tired
OK, In this blog we’re going to learn the correct way.
Super: Until now I’ve learned many incorrect things….
No, no, that was a planned learning experience
Sure, nice buzz words…

Goal

Write a simple node.js application
NEW: It should programmatically fetch the xsuaa credentials
Then call API, like before

Prerequisites

This blog builds on top of the following previous blogs:

Calling API from external tool
Calling API from external node.js app
Calling API from internal node.js app, deployed to Cloud Foundry

The OAuth Blog is not a prerequisite – it’s just a great blog, so I advertise it here
Ejem … is advertising allowed…???

Preparation

We’re going build on top of the sample code 2 of previous tutorial
You can copy it over to a new folder

Create XSUAA instance

It is possible to reuse existing xsuaa instance of previous blogs, which were used for external app
However, to see the difference, let’s create a new instance:

Create xsuaa service instance (service plan “application”) with name “XsuaaForNodeApp” and use the same params like before:

{
  "xsappname": "<yourAppName>",
  "foreign-scope-references": [
    "$XSAPPNAME(application,4bf2d51c-1973-470e-a2bd-9053b761c69c,Backend-service).AllAccess"
  ]
}

No difference until now
BUT the difference is that NOW we DON’T create a service key.
Why not?
Not necessary, because we bind the service instance to our app.
Which app?
OK, since the app is not deployed yet – even not written yet – we have to bind it later
As such: NO need to take a note of the params like clientid etc
Sure, we don’t even see them
Not yet

Create Application

Create a new folder C:\tmp_bsnode_bnd
Step into this folder

The manifest file

In this folder create a manifest.yml file with the following content

---
applications:
- name: myBsConsumerApp
  host: bsappwithbinding
  path: appfolder
  memory: 64M
  buildpack: nodejs_buildpack
  services:
    - XsuaaForNodeApp

I see 2 diffs: no start command, but new “services” entry

Explanation:

The attribute “services” allows us to specify:
We want to bind our app to existing service instance(s)
The value of that attribute is a list
A list of possible existing service instances
As such, we give the name of the xsuaa service instance which we created above

Note:
If the service instance doesn’t exist, the deployment will fail, so make sure to correctly type the name

Note:
This time we don’t specify a “start command”. Instead, we write it into a package.json file

The package.json file

We create a new subfolder with name appfolder and step into it
We create a file with name package.json and the following content:

{
  "name": "bsconsumer",
  "version": "1.0.0",
  "description": "This app calls Backend service API",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "client-oauth2": "^4.2.4",
    "express": "^4.17.0",
    "request": "^2.88.0",
    "request-promise": "^4.2.4"
  }
}

Explanation:

The package.json file declares metadata about the application.
Important: it lists the dependencies
Furthermore it declares the “start” script which is executed by npm.
In our case, it is useful, because after deployment to the cloud, the node.js buildpack will try to run npm start to start the application. That’s the mechanism which we want to use this time
The value of the start script is not surprising: it uses node to run the only javascript file which we have.

Install the dependencies

This time we have a package.json file, which makes it much easier for us to install dependencies.
Reason: npm reads the package.json file and installs all dependencies which are declared there
As such, go to command prompt, navigate to the appfolder directory and execute the following command:

npm install

That’s it.
You can also run npm install –save
This has the advantage that it will save the newest version to the package.json file

Create app file

So now it is time to create the app.js file
In the directory appfolder, create a second file with name app.js
Paste the following content

'use strict';

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

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

// Cloud xsuaa service: accessed via VCAP prerequisite: bind app to xsuaa service 
const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES);
const XSUAA_URL = VCAP_SERVICES.xsuaa[0].credentials.url; 
const XSUAA_CLIENTID = VCAP_SERVICES.xsuaa[0].credentials.clientid; 
const XSUAA_CLIENTSECRET = VCAP_SERVICES.xsuaa[0].credentials.clientsecret; 

const _getAccessToken = function() {
    return new Promise((resolve, reject) => {
        const client = new oauthClient({
            accessTokenUri: XSUAA_URL + '/oauth/token',
            clientId: XSUAA_CLIENTID,
            clientSecret: XSUAA_CLIENTSECRET,
            scopes: []
        });
    
        client.owner.getToken('yourUser@yourMmail.com', 'yourPwd123')
        .catch((error) => {
            reject({message: 'Error: failed to get access token', error: error}); 
            return;       
        })
        .then((result) => {
            resolve({message:'Successfully fetched OAuth access token: ' + result.accessToken.substring(0,11) + '...', 
            accessToken: result.accessToken});
        });
    });
}

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 server
const port = process.env.PORT || 3000;  // cloud foundry will set the PORT env after deploy
app.listen(port, function () {
    console.log('=> Server running. Port: ' + port);
    console.log(port);
})

// server endpoint: on incoming request, call Backend service API and render the result in browser
app.get('/', function (req, res) {
    console.log('Request received. Now calling OData service...');

    _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,64));
        res.send('<h2>RESULT:</h2>Called OData service QUERY and received response: <p>' + JSON.stringify(result.responseBody) + '</p>');
    })
    .catch((error) => {
        console.log(error.message + ' Reason: ' + error.error);
        res.send('ERROR: ' + error.message + ' - FULL ERROR: ' + error.error);
    });    
});

Note:
Make sure to adapt the code: enter your Trial account user and password

Explanation:

Everything is the same like in the previous tutorial
The only difference:
This time, the client credentials params are not hard-coded, not pasted from cloud cockpit
They’re accessed from environment variable:

process.env.VCAP_SERVICES

We’ll check that later

Deploy application

Now that we’ve created the necessary 3 files, we’re ready to deploy
So let’s first create the archive file:
As described in the previous blog:
In the appfolder directory, select  all files and the node_modules folder, open the context menu and choose “Add to appfolder.zip”

Then deploy the app using the cloud cockpit like described in the previous blog (alternatively use the command line client)
After some time, the deployed app should get green state

Check the deployed app

Click on the app name hyperlink to go to the details page of the deployed application
On the left navigation pane, there’s a menu entry “Service Bindings” which you can click now

The “Service Bindings” screen is shown
It shows all services which are bound to this application

In our case, there’s only the one service instance of xsuaa
If you dare, you can click on “Show sensitive data”. It will give you the params which you would have gotten explicitly, if you would have created a service key
If you click on “xsuaa”, it will take you to the overview page of the service definition (like when accessed from the “Service Marketplace”), from where you can also access the list of service instances
If you click on “XsuaaForNodeApp”, it will take you directly to the service instance page.
There, you can click on the app name to go back to our app
Cool, like that I can spend hours, clicking around in circles.
BUT: please not now, we have some work to do

Note:
Instead of declaring the binding in the manifest.yml, it can be done manually here, pressing the button “Bind Service”
Alternatively: deploy app without binding, then create the service instance and choose the app in the wizard

In the left navigation pane, click on “Environment Variables”

Here you can see the environment variables which are provided by Cloud Foundry to the app, so we can use them
And that’s what we’re doing in our node.js application code (as mentioned above)
Since the environment variables are provided as JSON object, we can access them like we’re used to deal with JSON objects:

const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES);
const XSUAA_URL = VCAP_SERVICES.xsuaa[0].credentials.url; 

Note:
If you like, you can add a console.log() statement to check that the value which we read is the same like the value which we see here or when clicking on “Show sensitive data”

Note:
The structure of the JSON object in the VCAP has to be figured out before writing above code

Note:
If you don’t bind the xsuaa instance to the app, then you don’t get that information in the VCAP_SERVICES

Note:
The information given in the VCAP is specific to the bound service

Note:
More information:
Cloud Foundry documentation about environment variables
How to access environment variables programmatically from node.js

Run Application

OK.
Now that the app is deployed, finally let’s run the application
Why?
Hm, anyways, we don’t expect any other output like in the previous tutorial
OK, so let’s skip it
Fine

Note:
If case you still want to run the application, to check if it still works with the programmatically fetched client credentials…
Go to the application overview page and click on the “Application Route”
And voilà, there it is the nice service response
No screenshot?
See there

Summary

In this tutorial we’ve learned:
A deployed app can be bound to a service instance, e.g. XSUAA
As a consequence, the specific service info is added to the ENV of the app
As such, it can be accessed from the application code

The binding can be specified in the manifest file

Finally we’ve learned the correct way of calling a service which is protected with OAuth 2
Great, finally learned the correct way, no more next steps..

BTW:
There’s an even more correct way of doing it…. Will be described in a future blog
So stay tuned…

Aargh, still need to follow more blogs….

Links

Overview of blogs
Calling API from external tool
Calling API from external node.js app
Calling API from internal node.js app, deployed to Cloud Foundry
Configure security

Cloud Foundry documentation about environment variables
How to access environment variables programmatically from node.js

Appendix: Quick Reference

XSUAA service params

{
  "xsappname": "<yourAppName>",
  "foreign-scope-references": [
    "$XSAPPNAME(application,4bf2d51c-1973-470e-a2bd-9053b761c69c,Backend-service).AllAccess"
  ]
}

Binding syntax in manifest

---
applications:
- name: myAppName
  . . .
  services:
    - serviceInstanceName

Access ENV in node.js

const VCAP = JSON.parse(process.env.VCAP_SERVICES);
const URL = VCAP.xsuaa[0].credentials.url; 
Be the first to leave a comment
You must be Logged on to comment or reply to a post.