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-testThen initialise node:
$ npm init -yInstall the necessary packages:
$ npm install express express-fileupload -sdk/client-s3 cors dotenvCreate 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 publicThe 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.jsfire up localhost:3000
Make sure that you have a bucket created in AWS:
Click on ‘browse’
Then click on ‘Upload to AWS’
You should then see a message like this:
And you should also see it in the ‘Recent uploads’ list:
Have a check in the S3 Bucket:
Super!
Now, let’s see if we can deploy this app to Cloud Foundry:
$ cf push csp-testShould 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 :
Load up your app from ‘Application Routes’:
You should see something like this:
Try to upload a file:
Uploaded successfully:
Check in AWS S3 - Splendid, it's there!
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.