Technology Blog Posts by Members
cancel
Showing results for 
Search instead for 
Did you mean: 

I’m new to CAP, BTP, and Cloud Foundry, and I’ll admit it took some effort to get everything working smoothly at first. This guide is designed to help you get started quickly with a minimal Node.js application deployed to Cloud Foundry that uploads files to an AWS S3 bucket.

To follow along, you’ll need access to SAP BTP (a trial account is sufficient), AWS (the free tier works fine), and a basic understanding of NPM commands. An IDE such as Visual Studio Code is recommended but not strictly required.

The Node.js Server

Create a working folder:

$ mkdir csp-test
$ cd csp-test

Then initialise node:

$ npm init -y

Install the necessary packages:

$ npm install express express-fileupload -sdk/client-s3 cors dotenv

Create a .env file

To hold your AWS credentials (never hardcode them in a script or application) :

AWS_REGION=<AWS_REGION>

AWS_BUCKET=<AWS_BUCKET>

AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY

AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY

PORT=3000

(Your keys , AWS region & bucket name go here.)

Create server.js

// server.js
const express = require("express");
const fileUpload = require("express-fileupload");
const cors = require("cors");
const dotenv = require("dotenv");
const path = require("path");
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");

dotenv.config();

const app = express();
app.use(cors());
app.use(fileUpload());
app.use(express.static(path.join(__dirname, "public")));

app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

app.post("/upload", async (req, res) => {
  if (!req.files || !req.files.file) {
    return res.status(400).send("No file uploaded");
  }

  const file = req.files.file;

  // create the S3 client
  const s3 = new S3Client({
    region: process.env.AWS_REGION,
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    },
  });

  const params = {
    Bucket: process.env.AWS_BUCKET,
    Key: file.name,
    Body: file.data,
    ContentType: file.mimetype,
  };

  try {
    await s3.send(new PutObjectCommand(params));
    const fileUrl = `https://${process.env.AWS_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${file.name}`;
    console.log("Uploaded:", fileUrl);
    res.json({ message: "Upload successful", fileUrl });
  } catch (err) {
    console.error("Upload failed:", err);
    res.status(500).json({ error: "Upload failed", details: err.message });
  }
});

const PORT = process.env.PORT;
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));

The Frontend (UI5 Fiori)

From the root of the project:

$ mkdir public

The folder structure should look like this:

csp-test/

├── server.js

├── .env

├── package.json

└── public/

    └── index.html

(Everything in public/ is served automatically by Express)

The working Fiori UI (public/index.html)

This gives you:

  • Upload to /upload
  • Progress bar
  • “Recent uploads” list
<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="UTF-8" />

    <title>Fiori AWS S3 Upload Portal</title>

    <script

      id="sap-ui-bootstrap"

      src="https://openui5.hana.ondemand.com/resources/sap-ui-core.js"

      data-sap-ui-theme="sap_fiori_3"

      data-sap-ui-libs="sap.m,sap.ui.unified,sap.f"

      data-sap-ui-compatVersion="edge"

      data-sap-ui-preload="async"

    ></script>

    <script>

      sap.ui.getCore().attachInit(function () {

        sap.ui.require(

          [

            "sap/f/ShellBar",

            "sap/m/App",

            "sap/m/Page",

            "sap/m/VBox",

            "sap/m/Button",

            "sap/m/List",

            "sap/m/StandardListItem",

            "sap/m/MessageBox",

            "sap/m/MessageToast",

            "sap/m/ProgressIndicator",

            "sap/ui/unified/FileUploader",

            "sap/f/Card",

            "sap/f/cards/Header",

            "sap/f/cards/HeaderPosition",

          ],

          function (

            ShellBar,

            App,

            Page,

            VBox,

            Button,

            List,

            StandardListItem,

            MessageBox,

            MessageToast,

            ProgressIndicator,

            FileUploader,

            Card,

            CardHeader,

            HeaderPosition

          ) {

            const uploads = [];

            const oProgress = new ProgressIndicator({

              percentValue: 0,

              displayValue: "0%",

              width: "100%",

              state: "Information",

              visible: false,

            });

            const oUploader = new FileUploader({

              name: "file",

              uploadUrl: "/upload",

              sendXHR: true,

              width: "100%",

              tooltip: "Select a file to upload to AWS S3",

              change: (e) => {

                const file = e.getParameter("files")[0];

                if (file) {

                  MessageToast.show("Selected: " + file.name);

                  oProgress.setVisible(true);

                  oProgress.setPercentValue(0);

                  oProgress.setDisplayValue("0%");

                }

              },

              uploadStart: () => {

                oProgress.setVisible(true);

                oProgress.setPercentValue(10);

                oProgress.setDisplayValue("Uploading...");

              },

              uploadProgress: (e) => {

                const percent =

                  (e.getParameter("loaded") / e.getParameter("total")) * 100;

                oProgress.setPercentValue(percent);

                oProgress.setDisplayValue(Math.floor(percent) + "%");

              },

              uploadComplete: (e) => {

                const response = e.getParameter("responseRaw");

                try {

                  const data = JSON.parse(response);

                  if (data.fileUrl) {

                    oProgress.setPercentValue(100);

                    oProgress.setDisplayValue("Done");

                    oProgress.setState("Success");

                    uploads.unshift({

                      name: data.fileUrl.split("/").pop(),

                      url: data.fileUrl,

                    });

                    oList.removeAllItems();

                    uploads.slice(0, 5).forEach((f) =>

                      oList.addItem(

                        new StandardListItem({

                          title: f.name,

                          description: f.url,

                          type: "Active",

                          icon: "sap-icon://document",

                          press: () => window.open(f.url, "_blank"),

                        })

                      )

                    );

                    MessageBox.success(" Uploaded to S3:\n" + data.fileUrl);

                  } else {

                    oProgress.setState("Error");

                    MessageBox.error("Upload failed:\n" + response);

                  }

                } catch (err) {

                  oProgress.setState("Error");

                  MessageBox.error("Unexpected response:\n" + response);

                }

              },

            });

            const oUploadBtn = new Button({

              text: "Upload to AWS S3",

              type: "Emphasized",

              press: () => oUploader.upload(),

            });

            const oList = new List({

              headerText: "Recent Uploads",

              noDataText: "No files uploaded yet",

            });

            const oCard = new Card({

              width: "420px",

              header: new CardHeader({

                title: "File Upload",

                subtitle: "Upload files directly to AWS S3",

                icon: "sap-icon://upload-to-cloud",

                statusText: "Ready",

                position: HeaderPosition.Top,

              }),

              content: new VBox({

                alignItems: "Stretch",

                items: [oUploader, oProgress, oUploadBtn],

              }).addStyleClass("sapUiContentPadding"),

            });

            const oPage = new Page({

              title: "AWS S3 Upload (Fiori App)",

              content: [

                new VBox({

                  alignItems: "Center",

                  justifyContent: "Center",

                  items: [oCard, oList],

                }).addStyleClass("sapUiLargeMargin"),

              ],

            });

            const oShellBar = new ShellBar({

              title: "Fiori Upload Portal",

            });

            const oApp = new App({ pages: [oPage] });

            oShellBar.placeAt("shell");

            oApp.placeAt("content");

          }

        );

      });

    </script>

  </head>

  <body class="sapUiBody sapUiSizeCompact">

    <div id="shell"></div>

    <div id="content"></div>

  </body>

</html>

Running and testing

 $ node server.js

fire up localhost:3000

neilaspin_0-1760183305837.png

Make sure that you have a bucket created in AWS:

neilaspin_1-1760183305842.png

Click on ‘browse’

neilaspin_2-1760183305844.png

Then click on ‘Upload to AWS’

You should then see a message like this:

neilaspin_3-1760183305845.png

And you should also see it in the ‘Recent uploads’ list:

neilaspin_4-1760183305849.png

Have a check in the S3 Bucket:

neilaspin_5-1760183305854.png

Super!

Now, let’s see if we can deploy this app to Cloud Foundry:

$ cf push csp-test

Should see some output like this:

Waiting for app csp-test to start...

Instances starting...

Instances starting...

Instances starting...

Instances starting...

Instances starting...

name:                csp-test

requested state:     started

isolation segment:   trial

routes:              <route_to_your_instance>

last uploaded:       Sat 11 Oct 12:13:05 BST 2025

stack:               cflinuxfs4

buildpacks:         

isolation segment:   trial

                  name               version   detect output   buildpack name

                  nodejs_buildpack   1.8.39    nodejs          nodejs

type:            web

sidecars:       

instances:       1/1

memory usage:    1024M

start command:   npm start

     state     since                  cpu    memory   disk     logging      details

#0   running   2025-10-11T11:13:19Z   0.0%   0 of 0   0 of 0   0/s of 0/s 

Check running apps in BTP :

neilaspin_6-1760183305860.png

Load up your app from ‘Application Routes’:

neilaspin_7-1760183305862.png

You should see something like this:

Screenshot 2025-10-11 at 13.07.48.png

Try to upload a file:

Screenshot 2025-10-11 at 15.07.53.png

Uploaded successfully: 

Screenshot 2025-10-11 at 15.30.23.png

Check in AWS S3 - Splendid, it's there!

Screenshot 2025-10-11 at 15.32.13.png

 

Conclusion

This walkthrough keeps things deliberately simple — no frameworks, no unnecessary layers, just a clean Node.js setup that talks directly to AWS S3 and runs smoothly on SAP BTP Cloud Foundry.

If everything worked, you now have a working upload portal with only the essentials: a Node.js backend, a single-page Fiori UI, and an S3 bucket to store your files. Nothing hidden, nothing over-engineered.

From here, you can start adding what you actually need — authentication, CAP services, database persistence, or a Kyma deployment. But it’s worth understanding this bare-bones setup first, because when something breaks (and it will), you’ll know exactly where to look.

The goal wasn’t to build something flashy — It was to make the simplest possible app that works end to end...If you’ve done that, you’ve already learned the hardest part.

5 Comments
Labels in this area