Skip to Content
Technical Articles

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

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

Quicklinks:
manifest.yml
Deployment
Sample 2

In today’s tutorial, we take the next step:
We take the app which we created in the previous tutorial and deploy it to Cloud Foundry. More precisely: SAP Cloud Platform, Cloud Foundry Environment
This tutorial doesn’t contain any special and even no interesting content – it is meant for those of you who have never deployed a node.js application to SAP Cloud Platform, Cloud Foundry Environment

Goal

Write a simple application with node.js
It should have these 2 features:
– It should run in the cloud
– It should call an API created in Backend service

Prerequisites

You have successfully run the application in the previous tutorial
You have some memory quota in your subaccount for deploying one little application
How to check?
Go to your sub account, click on Spaces and check your space info:
There should be enough quota for one more app and one more route

Preparation

Our goal is to write a simple node.js application which runs in Cloud Foundry
I think the below description is the easiest possible way.
We just reuse the simple app.js file from previous tutorial.
Absolutely no change to the code required
As such, in this blog we learn only how to cloudify it

Make sure that you have the artifacts of the previous tutorial at hand:
We need the app.js file and the node_modules folder

Note:
You can also use the enhanced sample code

Cloudify the app

Create a new folder for this tutorial:

C:\tmp_bsnode_cf

Step into this folder.
Create another folder with name:

\appfolder

Step into this folder
We’re now inside the folder

C:\tmp_bsnode_cf\appfolder

Copy the app.js file from previous blog and paste it into this folder
Copy the node_modules folder from previous blog and paste into this folder

Note:
Of course, you can also install the node packages with npm as described in previous blog

The archive file

Still inside the same folder, compress all the content into an archive called appfolder.zip
(select the file and the folder, open context menu and choose “Add to appfolder.zip”)
Now the content of the folder looks like this:

Note:
We create the zip file because it is needed for deployment from cockpit.
When using the command line client for deployment, zipping is not required
However, for the beginning it is easier to use the cloud cockpit

The manifest file

Now we step out of this folder and move one level higher.
We’re now in the folder

C:\tmp_bsnode_cf

Create a file with name

manifest.yml

Note:
This is not a name of your choice. Make sure it is written correctly

Now the folder looks like this:

Open the manifest.yml file with notepad and paste the following content:

---
applications:
- name: myBsConsumerApp
  host: bsapphost
  path: appfolder
  command: node app.js
  memory: 64M
  buildpack: nodejs_buildpack

Note:
The Open the yml format is very sensitive, so make sure that you don’t miss any blank or CR
That’s why it is a good idea to use Notepad, because other editors might add invisible characters which would break the file and cause errors during deployment

Note:
The Open the manifest.yml file is like a deployment descriptor, containing metadata about the deployed app and the environment

Explanation of attributes:

applications:
We’re deploying one app, but there could be more apps deployed with one descriptor

name:
A name of your choice for your app. This name will appear in the cloud cockpit

host:
A name of your choice for your host.
This attribute is optional, the default host name is the application name
The host name will end up as prefix in the final URL of our application.
A final URL looks like this:
https://myhost.cfapps.eu10.hana.ondemand.com/
As you can see, myhost has to be unique, there’s no personal specific account information in the URL
Note:
You really SHOULD change this name, as it must be unique.
Yes, millions of enthusiastic fans of my blogs will deploy applications… so they will clash if everybody uses the same host

path:
The relative path to the folder which contains the app.
Relative path means relative to the manifest.yml location

command:
In case of a node application, the Cloud Foundry buildpack (node.js) needs to know how the application should be started.
For that purpose, we add the start-command attribute
Note:
There are other ways of specifying the start command. Furthermore, there are also defaults.
But let’s use this way: it makes things clear, no hidden magic

memory:
With this attribute we can tell Cloud Foundry that we don’t need more memory than 64MB. Otherwise, the platform would assume we need the maximum, which is 1 GB
It is an optional attribute

buildpack:
If not specified, the platform tries to guess the correct buildpack.
An indicator would be the package.json file, (which we’re still ignoring)
Let’s add this attribute as well, it helps to make magic things visible

Note:
See here for full reference of attributes

After creating a manifest and zipping the content, we’re ready to deploy the node.js application to the cloud.

Deploy

In this description we’re using the SAP Cloud Platform cockpit for deployment
It is less cool….
But easier, because it doesn’t require installation of the command line tool (see here)

The “Deploy” button can be found in the “applications” screen:
Go to your Subaccount ( that one, where the Backend service is subscribed)
Go to your Space
There it is:

Press the button.
In the “Deploy Application” dialog, enter the path to the zip file and to the manifest.yml file

Press “Deploy”
The deployment process is triggered.
Don’t wait until the “State” gets green.

Click on the app name to go into the application details screen

Don’t wonder that the application state is “Starting” and also “crashed”.
And don’t click on the “Application Route”, as it doesn’t work for us

Result

Our node.js application is not a web application, so we cannot expect any meaningful behavior.
We just wanted to easily gain first Cloud Foundry deployment experience
Remember: our sample app writes just few messages to the log.
So we have to search for our success in the logs.
Click on the “Logs” entry in the navigation pane on the left side

Note:
If you get an error message saying “Unable to get application logs…”, then just refresh the browser

Scroll down until you see the log output of our application:

That’s it:
We can see that the application has been able to get the OAuth token and call the Backend service API
Successfully.
And it has written a few success messages to the log.
Successfully.
Our application works as expected
Everything has crashed… but we’re happy…

Note:
To easier find the log output, you can filter the log table, e.g. by Log Type:

Note:
There’s a spectacle icon which allows to see the full log entry

Internal vs. external

The title of this blog was chosen to differentiate against the previous blogs:
When calling APIs from “external tool” or “external node app“, we were calling from local laptop to the cloud.
In the present blog, we’re calling from cloud to cloud: our app is deployed to the cloud and is hosted in the same identity zone like the OData service which is called.
That’s why we can say that we call the API from “internal app”.
BUT:
In our code we’re re-using the security mechanism which was required for external app:
There’s the xsuaa instance and the service key.
Note that creating a service key is required for external usage, to get the clientid etc
In case of internal usage, it is not necessary to create service key. Instead, the clientid and other values are made available to the app itself, when binding it to xsuaa.
We’ll discuss that in the next blog.

I just wanted to make clear that in this blog we aren’t doing things in the right way.

Call API from different subaccount

You can try one thing:
Deploy the same app to a different subaccount.
If you’ve followed my blogs, then you might have 2 subaccounts in your Trial account:
The “trial” subaccount and the second one which we created in order to enable BETA features for early usage of Backend service.
As such, you can go to the “trial” subaccount and to the Space there (if you don’t have, then you can create one. See here how to assign quota).
Deploy the application there.
Result: it should work same way.
Reason: We’re using valid clientid and also the user can be authenticated by the cloud platform

However, in this special case, I guess we would have to call the scenario: call API from external node app

Summary

We’ve reused our simple node.js application which writes to the log
In order to deploy the app, we have to add a manifest file
If we want to use the cloud cockpit for deployment, we have to zip the app content

Links

Info about manifest file in Cloud Foundry:
https://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html

Info about attributes of manifest file:
https://docs.cloudfoundry.org/devguide/deploy-apps/manifest-attributes.html

Info about start command in manifest faile:
https://docs.cloudfoundry.org/buildpacks/node/node-tips.html#start

UAA concepts in Cloud Foundry: Identity zone

YAML Info: https://en.wikipedia.org/wiki/YAML
YAML validator: https://yamlvalidator.com/

Appendix 1: Deployment

About the zip:

Zip is required when deploying from cockpit
Zip file contains the app code
In manifest file, the name of the zip is specified
That can be adapted, as the path property allows to specify a relative path
Note that the zip is extracted and the zip file name becomes the folder name

Using command line instead of cockpit:

When using the command line client tool for deployment, then it is not necessary to zip

Furthermore, when using the command line client, it is not necessary to deploy the node_modules folder.
In the cloud, the npm install will be executed
Prerequisite: The package.json file must be present, in order to declare the dependencies

The start command:

After deployment, the Cloud Foundry buildpack tries to run
npm start
Prerequisite:
package.json file with entry like  “scripts”: { “start”: “node app.js” }

Alternative:
If there’s a server.js file then npm will use that as start command.
Means,the default is
“scripts”: { “start”: “node server.js” }
See here

Alternative:
Add the “start” command to the manifest file.
That’s what we’ve done in this blog

Appendix 2: Code sample with endpoint

To make the node.js application a little bit more meaningful, we can add webserver functionality to our application. Like that, we get an endpoint which we can call with the browser and we see the result in the browser window instead of the log
Furthermore, the app won’t crash after deploymet

First, install the node package “express”

npm install express

Then overwrite your app.js file with 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';

const _getAccessToken = function() {
    return new Promise((resolve, reject) => {
        const oautClient = new oauthClient({
            accessTokenUri: 'https://bssubaccount.authentication.eu10.hana.ondemand.com/oauth/token',
            clientId: 'sb-forAuthCodeWithScope!t13020',
            clientSecret: 'VZz530437dJ6eTVmigKhfGG89lw=',
            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}); 
        });
    });
}

 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 });
        });
    });
 };

// server response
app.get('/', function (req, res) {  
    console.log('Node server has been invoked. Now 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,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);
    });    
});

// the server
const port = process.env.PORT || 3000;  // cloud foundry will set the PORT env after deploy
app.listen(port, function () {
    console.log('Node server running. Port: ' + port);
    console.log(port);
})

The difference to the above app:

This time we define a web server
It listens to GET requests at the root URL
It responds with the result of the Backend service API

Does it make sense?
Of course not.
But at least we have a valid web application which we can invoke in the browser

OK.
Re-create the zip file and deploy the app
This time, in the cloud cockpit, the status should turn to green
Go into the app details page and click on the URL in the Application Routes section:
bsapphost.cfapps.eu10.hana.ondemand.com

The result is presented immediately:

Be the first to leave a comment
You must be Logged on to comment or reply to a post.