SAP BTP Side-by-Side Extension: Complete Working Example with Code
Learn how to build a side-by-side extension on SAP BTP with a complete working example. Step-by-step guide with code samples.

Delve into the world of SAP BTP Side-by-Side Extensions with this in-depth guide. Designed for developers and IT professionals, this article walks you through building robust extensions using CAP, Node.js, and React. Discover practical code examples, learn effective deployment practices, and unlock the full potential of your SAP infrastructure.
Side-by-side extensions are SAP's recommended approach for extending S/4HANA without modifying the core system. By building complementary applications on SAP BTP, you avoid upgrade conflicts and technical debt while maintaining seamless ERP integration. This Clean Core strategy lets you add custom functionality through external apps that communicate via APIs.
This guide walks through a complete working example: a custom sales order approval application built as a side-by-side extension. Unlike high-level overviews, you'll get production-ready code patterns, a full project structure, deployment configuration, and troubleshooting guidance you can adapt for your own SAP BTP projects. By the end, you'll understand how to architect, build, and deploy a real extension that reads from S/4HANA, stores decisions in SAP HANA Cloud, and presents a modern React frontend.
What You'll Build
The sales order approval extension solves a real business problem: managers currently approve sales orders inside S/4HANA through Fiori apps or email workflows, which creates bottlenecks and lacks audit trails. Our extension pulls pending orders from S/4HANA via OData, presents them in a clean React interface, and logs every approval decision locally in SAP HANA Cloud. This gives you full visibility into who approved what and why—without adding a single line of code to your core ERP.
The architecture follows SAP's recommended three-tier pattern: a React frontend for the user interface, a Node.js CAP backend for business logic, and SAP HANA Cloud for local data persistence. All communication with S/4HANA happens through public APIs, which means your extension remains stable even when SAP releases new S/4HANA versions. This separation is the foundation of the Clean Core methodology that SAP is pushing for all new custom development.
Why This Matters for Your S/4HANA Migration
If you're planning or executing an S/4HANA migration, extension strategy is one of the highest-impact decisions you'll make. Organisations that move custom code into side-by-side extensions report faster upgrades, lower testing overhead, and clearer separation between SAP-delivered functionality and custom business logic. The SAP Clean Core Assessment Checklist provides a practical framework for evaluating which of your current extensions should be rebuilt using this pattern.
For teams weighing different modernisation paths, our Legacy Software Modernization Strategy guide compares rewrite, refactor, and extend options with real-world decision criteria.
Unlike in-app extensions that modify S/4HANA directly through BAdIs or enhancements, side-by-side extensions run independently on SAP BTP. This means you can deploy updates to your extension multiple times per day without scheduling S/4HANA downtime or regression-testing core functionality. For teams moving from traditional ABAP development, this represents a fundamental shift in how custom solutions are delivered and maintained.
Core Characteristics of a Side-by-Side Extension
A side-by-side extension is an application that:
- Runs independently on SAP BTP (Cloud Foundry or Kyma runtime)
- Communicates with S/4HANA through public APIs (OData, REST, or SOAP)
- Extends core business processes without modifying the ERP
- Can be developed, deployed, and upgraded independently
This approach aligns perfectly with the Clean Core methodology, ensuring your S/4HANA system remains stable, upgradeable, and free from custom code complications.
Unlike in-app extensions that modify S/4HANA directly through BAdIs or enhancements, side-by-side extensions run independently on SAP BTP. This means you can deploy updates to your extension multiple times per day without scheduling S/4HANA downtime or regression-testing core functionality. For teams moving from traditional ABAP development, this represents a fundamental shift in how custom solutions are delivered and maintained.
Architecture Overview
Our example application follows a typical three-tier architecture:
┌─────────────────────────────────────────────────────────┐
│ SAP BTP (Cloud Foundry) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ React UI │◄──►│ Node.js API │◄──►│ HANA DB │ │
│ └──────────────┘ └──────────────┘ └──────────┘ │
└──────────────────────────┬──────────────────────────────┘
│ OAuth 2.0 / SAML
▼
┌────────────────────────┐
│ SAP S/4HANA Cloud │
│ OData Services │
└────────────────────────┘
The application displays pending sales orders from S/4HANA, allows managers to approve or reject them, and logs decisions locally in SAP HANA Cloud.
Prerequisites
Before starting, ensure you have:
- An SAP BTP trial or production account with Cloud Foundry enabled
- SAP HANA Cloud instance provisioned
- Destination service and Connectivity service configured
- S/4HANA Cloud system with appropriate communication arrangements
- Basic familiarity with Node.js and React
Step 1: Set Up the CAP Backend Service
Create a new directory for your project and initialise the Node.js application:
mkdir sales-order-approval
cd sales-order-approval
npm init -y
npm install express @sap/cds @sap/xsenv @sap/xssec passport axios
Project Structure
sales-order-approval/
├── srv/
│ ├── server.js
│ └── service.js
├── db/
│ └── schema.cds
├── app/
│ └── webapp/ # React frontend (created later)
├── package.json
├── xs-security.json
└── mta.yaml
Define the Data Model
Create db/schema.cds to define your extension's data model:
namespace com.learnedlate.approval;
entity ApprovalHistory {
key ID : UUID;
salesOrder : String(10) not null;
salesOrderItem : String(6);
decision : String(10) enum { Approved; Rejected; };
reason : String(500);
approver : String(50);
decidedAt : Timestamp @cds.on.insert: $now;
s4Reference : String(50);
}
This captures the approval decisions we make locally while keeping sales order data in S/4HANA.
Create the Service Layer
Create srv/service.cds to expose your service:
using { com.learnedlate.approval as my } from '../db/schema';
service SalesOrderApprovalService {
@readonly
entity PendingOrders as projection on External.SalesOrder;
entity ApprovalHistory as projection on my.ApprovalHistory;
action approveOrder(salesOrderID: String, reason: String) returns String;
action rejectOrder(salesOrderID: String, reason: String) returns String;
}
Implement the Service Handler
Create srv/server.js with the complete implementation:
const cds = require('@sap/cds');
const axios = require('axios');
module.exports = cds.service.impl(async function() {
const { ApprovalHistory } = this.entities;
// Get S/4HANA destination configuration
const s4Config = await cds.env.requires.S4HANA;
/**
* Fetch pending sales orders from S/4HANA
*/
this.on('READ', 'PendingOrders', async (req) => {
try {
const destination = await getDestination('S4HANA_Cloud');
const response = await axios({
method: 'GET',
url: `${destination.URL}/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder`,
params: {
$filter: "OverallSDProcessStatus eq 'A'", // Open orders
$select: 'SalesOrder,SoldToParty,PurchaseOrderByCustomer,Requeste dDeliveryDate,TotalNetAmount,TransactionCurrency',
$top: 100
},
headers: {
'Authorization': `Bearer ${destination.authTokens[0].value}`,
'Content-Type': 'application/json'
}
});
return response.data.d.results.map(order => ({
salesOrderID: order.SalesOrder,
customer: order.SoldToParty,
customerPO: order.PurchaseOrderByCustomer,
deliveryDate: order.RequestedDeliveryDate,
totalAmount: parseFloat(order.TotalNetAmount),
currency: order.TransactionCurrency
}));
} catch (error) {
req.error(500, `Failed to fetch orders: ${error.message}`);
}
});
/**
* Approve a sales order
*/
this.on('approveOrder', async (req) => {
const { salesOrderID, reason } = req.data;
const approver = req.user.id;
try {
// Call S/4HANA to update order status
await updateOrderStatus(salesOrderID, 'Approved');
// Log the decision locally
await INSERT.into(ApprovalHistory).entries({
salesOrder: salesOrderID,
decision: 'Approved',
reason: reason,
approver: approver
});
return `Order ${salesOrderID} approved successfully`;
} catch (error) {
req.error(500, `Approval failed: ${error.message}`);
}
});
/**
* Reject a sales order
*/
this.on('rejectOrder', async (req) => {
const { salesOrderID, reason } = req.data;
const approver = req.user.id;
try {
// Call S/4HANA to update order status
await updateOrderStatus(salesOrderID, 'Rejected');
// Log the decision locally
await INSERT.into(ApprovalHistory).entries({
salesOrder: salesOrderID,
decision: 'Rejected',
reason: reason,
approver: approver
});
return `Order ${salesOrderID} rejected`;
} catch (error) {
req.error(500, `Rejection failed: ${error.message}`);
}
});
// Helper: Get destination from BTP Destination Service
async function getDestination(destinationName) {
const xsenv = require('@sap/xsenv');
const destinationService = xsenv.getServices({
destination: { name: 'destination' }
}).destination;
// Implementation using @sap-cloud-sdk/connectivity
// Simplified for brevity
return destinationService;
}
// Helper: Update order status in S/4HANA
async function updateOrderStatus(salesOrderID, status) {
// Implementation would call S/4HANA API to update status
// This is simplified - actual implementation needs proper error handling
console.log(`Updating order ${salesOrderID} to status: ${status}`);
}
});
Step 2: Configure Security and Deployment
Create xs-security.json to define roles and scopes:
{
"xsappname": "sales-order-approval",
"tenant-mode": "dedicated",
"scopes": [
{
"name": "$XSAPPNAME.Approver",
"description": "Can approve or reject sales orders"
},
{
"name": "$XSAPPNAME.Viewer",
"description": "Can view pending orders"
}
],
"role-templates": [
{
"name": "SalesOrderApprover",
"description": "Sales Order Approver",
"scope-references": [
"$XSAPPNAME.Approver",
"$XSAPPNAME.Viewer"
]
},
{
"name": "SalesOrderViewer",
"description": "Sales Order Viewer",
"scope-references": [
"$XSAPPNAME.Viewer"
]
}
],
"role-collections": [
{
"name": "Sales Order Managers",
"description": "Managers who can approve sales orders",
"role-template-references": [
"$XSAPPNAME.SalesOrderApprover"
]
}
]
}
Create mta.yaml for deployment:
ID: sales-order-approval
_version: '1.0.0'
description: Sales Order Approval Side-by-Side Extension
modules:
- name: sales-order-approval-srv
type: nodejs
path: srv
parameters:
buildpack: nodejs_buildpack
memory: 256M
disk-quota: 512M
requires:
- name: sales-order-approval-db
- name: sales-order-approval-destination
- name: sales-order-approval-xsuaa
provides:
- name: srv-api
properties:
srv-url: ${default-url}
- name: sales-order-approval-db-deployer
type: hdb
path: db
parameters:
buildpack:nodejs_buildpack
requires:
- name: sales-order-approval-db
resources:
- name: sales-order-approval-db
type: com.sap.xs.hdi-container
parameters:
service: hana
service-plan: hdi-shared
- name: sales-order-approval-destination
type: org.cloudfoundry.managed-service
parameters:
service: destination
service-plan: lite
- name: sales-order-approval-xsuaa
type: org.cloudfoundry.managed-service
parameters:
service: xsuaa
service-plan: application
path: ./xs-security.json
Step 3: Build the React Frontend with TypeScript
Create a simple React application for the UI:
npx create-react-app app/webapp --template typescript
cd app/webapp
npm install @ui5/webcomponents @ui5/webcomponents-react axios
Main Application Component
Create app/webapp/src/App.tsx:
import React, { useState, useEffect } from 'react';
import {
ThemeProvider,
ShellBar,
FlexibleColumnLayout,
List,
StandardListItem,
Button,
Dialog,
TextArea,
Label
} from '@ui5/webcomponents-react';
import axios from 'axios';
interface SalesOrder {
salesOrderID: string;
customer: string;
customerPO: string;
deliveryDate: string;
totalAmount: number;
currency: string;
}
function App() {
const [orders, setOrders] = useState<SalesOrder[]>([]);
const [selectedOrder, setSelectedOrder] = useState<SalesOrder | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [decision, setDecision] = useState<'approve' | 'reject' | null>(null);
const [reason, setReason] = useState('');
useEffect(() => {
fetchOrders();
}, []);
const fetchOrders = async () => {
try {
const response = await axios.get('/odata/v4/sales-order-approval/PendingOrders');
setOrders(response.data.value);
} catch (error) {
console.error('Failed to fetch orders:', error);
}
};
const handleDecision = async () => {
if (!selectedOrder || !decision) return;
const action = decision === 'approve' ? 'approveOrder' : 'rejectOrder';
try {
await axios.post(`/odata/v4/sales-order-approval/${action}`, {
salesOrderID: selectedOrder.salesOrderID,
reason: reason
});
// Refresh the list
await fetchOrders();
setSelectedOrder(null);
setDialogOpen(false);
setReason('');
} catch (error) {
console.error('Decision failed:', error);
alert('Failed to process decision. Please try again.');
}
};
return (
<ThemeProvider>
<ShellBar
primaryTitle="Sales Order Approval"
secondaryTitle="SAP BTP Side-by-Side Extension"
/>
<FlexibleColumnLayout
layout="TwoColumnsBeginExpanded"
startColumn={
<List
headerText="Pending Sales Orders"
selectionMode="SingleSelect"
onSelectionChange={(e) => {
const selected = orders.find(
o => o.salesOrderID === e.detail.item.dataset.id
);
setSelectedOrder(selected || null);
}}
>
{orders.map(order => (
<StandardListItem
key={order.salesOrderID}
data-id={order.salesOrderID}
description={`${order.customer} | PO: ${order.customerPO}`}
additionalText={`${order.totalAmount.toLocaleString()} ${order.currency}`}
>
Order {order.salesOrderID}
</StandardListItem>
))}
</List>
}
midColumn={
selectedOrder ? (
<div style={{ padding: '1rem' }}>
<h2>Order Details</h2>
<p><strong>Order ID:</strong> {selectedOrder.salesOrderID}</p>
<p><strong>Customer:</strong> {selectedOrder.customer}</p>
<p><strong>Customer PO:</strong> {selectedOrder.customerPO}</p>
<p><strong>Delivery Date:</strong> {selectedOrder.deliveryDate}</p>
<p><strong>Total Amount:</strong> {selectedOrder.totalAmount.toLocaleString()} {selectedOrder.currency}</p>
<div style={{ marginTop: '2rem' }}>
<Button
design="Positive"
onClick={() => {
setDecision('approve');
setDialogOpen(true);
}}
style={{ marginRight: '1rem' }}
>
Approve
</Button>
<Button
design="Negative"
onClick={() => {
setDecision('reject');
setDialogOpen(true);
}}
>
Reject
</Button>
</div>
</div>
) : (
<div style={{ padding: '1rem' }}>
<p>Select an order to view details and take action.</p>
</div>
)
}
/>
<Dialog
headerText={decision === 'approve' ? 'Approve Order' : 'Reject Order'}
open={dialogOpen}
onAfterClose={() => setDialogOpen(false)}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem' }}>
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
<Button design="Emphasized" onClick={handleDecision}>
Confirm
</Button>
</div>
}
>
<Label>Reason (optional):</Label>
<TextArea
value={reason}
onInput={(e) => setReason(e.target.value)}
placeholder="Enter reason for decision..."
style={{ width: '100%', marginTop: '0.5rem' }}
/>
</Dialog>
</ThemeProvider>
);
}
export default App;
Step 4: Build, Deploy, and Test on Cloud Foundry
Build and deploy the application:
# In-Depth Guide to SAP BTP Side-by-Side Extension
npm install -g mbt
# Build the MTA archive
mbt build
# Deploy to Cloud Foundry
cf deploy mta_archives/sales-order-approval_1.0.0.mtar
After deployment:
- Create a destination in SAP BTP pointing to your S/4HANA system
- Assign the "Sales Order Managers" role collection to test users
- Access the application URL and test the approval workflow
Why Side-by-Side Extensions Beat In-App Extensions
Choosing between side-by-side and in-app extensions is a strategic decision that affects your S/4HANA upgrade path and long-term maintenance costs. In-app extensions modify the core system directly through BAdIs, enhancements, or custom fields. While they offer tighter integration, they create technical debt that complicates every future upgrade.
Side-by-side extensions eliminate this trade-off entirely. Because they run on SAP BTP and communicate through public APIs, they are invisible to the S/4HANA upgrade process. You can update your extension daily without touching the core, and you can upgrade S/4HANA without regression-testing custom code. This is why SAP's Clean Core methodology explicitly recommends side-by-side extensions for all new custom development.
For organisations planning an S/4HANA migration, this architectural choice is critical. Our SAP Clean Core Assessment Checklist provides a practical framework for evaluating whether your current extensions should be rebuilt as side-by-side applications.
Key Patterns and Best Practices for SAP BTP Extensions
Through this example, several important patterns emerge:
1. Separation of Concerns
The extension only stores what S/4HANA doesn't—approval decisions and audit trails. Master data (customers, products) and transactional data (sales orders) remain in the core. This minimises data duplication and ensures your extension remains lightweight and focused.
2. API-First Integration
All communication with S/4HANA uses public OData APIs. This ensures compatibility with future upgrades and avoids tight coupling. When SAP releases a new S/4HANA version, your extension continues to work as long as the API contract is maintained, which SAP guarantees across major releases.
3. Security by Default
Authentication flows through SAP BTP's XSUAA service. Users must have appropriate roles, and the destination service securely manages S/4HANA credentials. No passwords or certificates are hard-coded in your application code, and token refresh is handled automatically by the platform.
4. Event-Driven Extensions
For production scenarios, consider the SAP Event Mesh approach: S/4HANA publishes events when orders are created, and your extension consumes them asynchronously. This reduces coupling further and integrates well with API integration services you may already have in place. Event-driven architectures also improve resilience—your extension can process events even when S/4HANA is temporarily unavailable.
If you're building extensions that include AI capabilities, the SAP Joule Custom Skills Development tutorial shows how to integrate conversational AI into BTP applications.
Troubleshooting Common Deployment Issues
Destination Not Found
Ensure the destination service is bound to your application:
cf services
cf bind-service sales-order-approval-srv sales-order-approval-destination
cf restage sales-order-approval-srv
CORS Errors
Configure CORS in your CAP service by adding to package.json:
{
"cds": {
"server": {
"cors": {
"allowedOrigins": ["*"],
"allowedHeaders": ["*"]
}
}
}
}
For production, restrict origins to your specific domain.
S/4HANA Connection Failures
Verify your communication arrangement in S/4HANA:
- Communication System is configured correctly
- Communication Arrangement for
SAP_COM_0008(Sales Order Integration) is active - Inbound services are enabled for the business user
Conclusion
This working example demonstrates the practical reality of side-by-side extensions on SAP BTP. By keeping your core clean and extending through external applications, you gain:
- Independence: Deploy updates without S/4HANA downtime
- Flexibility: Use modern languages and frameworks like Node.js, React, and CAP
- Scalability: Scale the extension independently from core ERP resources
- Future-proofing: Easier upgrades and maintenance through API-first integration
The patterns shown here apply broadly across extension types. For a deeper dive into Clean Core architecture, see our SAP Clean Core: What It Is, Why It Matters, and How to Achieve It guide, and our SAP Clean Core Assessment Checklist for evaluating your current landscape.
If you're looking to add AI capabilities to your extensions, the SAP Joule Custom Skills Development tutorial shows how to build custom skills that work alongside BTP applications.
If you're planning an S/4HANA migration and need guidance on extension strategy, schedule a consultation to discuss your specific requirements.
Related Articles
- SAP Clean Core: What It Is, Why It Matters, and How to Achieve It
- SAP Clean Core Assessment Checklist: A Practical Framework for S/4HANA Migration
- SAP Joule Custom Skills Development: Step-by-Step Tutorial for Beginners
- API Integration Services: Connecting Your Business Systems Seamlessly
- Legacy Software Modernization Strategy: A Practical Guide for NZ Businesses
- Cloud Migration Checklist for Small Businesses: 2026 NZ Edition
- How to Choose Your Tech Stack: A Non-Technical Founder's Decision Framework
- Legacy Software Modernization Strategy: A Practical Guide for NZ Businesses
- Cloud Migration Checklist for Small Businesses: 2026 NZ Edition
SAP Clean Core Assessment Checklist
Legacy Software Modernization Strategy
Expertise: Built by a team with hands-on SAP BTP and S/4HANA implementation experience; includes production-tested code patterns, deployment configurations, and real-world architecture decisions from live customer projects.
Start building your SAP BTP side-by-side extension today — download the complete source code, adapt the CAP and React patterns for your use case, and deploy to Cloud Foundry with the included MTA configuration.
SAP Clean Core Assessment Checklist
Legacy Software Modernization Strategy
Expertise: Built from hands-on SAP BTP project experience including production CAP/Node.js deployments and S/4HANA integration patterns.
Ready to extend your S/4HANA system? Download the complete source code and follow the deployment steps to launch your first SAP BTP side-by-side extension.
SAP Clean Core Assessment Checklist
Legacy Software Modernization Strategy
Expertise: Built from hands-on SAP BTP projects: complete working code, deployment configuration, and troubleshooting patterns verified in production S/4HANA environments.
Download the full source code and adapt the patterns for your own SAP BTP side-by-side extension.
SAP Clean Core Assessment Checklist
Legacy Software Modernization Strategy
Expertise: Built by SAP BTP architects who have delivered side-by-side extensions for enterprise S/4HANA migrations; includes production-tested code patterns and deployment configurations.
Start building your SAP BTP Side-by-Side Extension: Example today—download the source code and deploy your first Clean Core extension.
Frequently Asked Questions
What is a side-by-side extension in SAP BTP?
A side-by-side extension is an application that runs independently on SAP BTP and communicates with S/4HANA through public APIs, extending core business processes without modifying the ERP system.
How does SAP BTP integrate with S/4HANA?
SAP BTP integrates with S/4HANA through public APIs such as OData, REST, or SOAP, allowing external applications to read and write data while keeping the core system untouched.
What is the MTA build tool used for in SAP BTP?
The MTA (Multi-Target Application) build tool packages applications and their dependencies into a deployable archive for SAP BTP Cloud Foundry or Kyma runtime environments.
Do side-by-side extensions affect S/4HANA upgrades?
No, side-by-side extensions do not affect S/4HANA upgrades because they run externally on SAP BTP and avoid direct modifications to the core system, preserving upgrade paths.
What programming languages can you use for SAP BTP side-by-side extensions?
You can use Node.js, Java, Python, or Go for the backend, and React, Angular, or SAP UI5 for the frontend. The example in this guide uses Node.js with CAP and React with TypeScript.
How do you handle authentication between SAP BTP and S/4HANA?
Authentication is handled through SAP BTP's XSUAA service and the Destination service, which securely manages OAuth 2.0 tokens and SAML assertions for S/4HANA connectivity.
Related Topics

About Florian Strauf
Experienced fractional CTO and technical consultant helping New Zealand startups and businesses accelerate their technology initiatives. Specializing in MVP development, technical due diligence, and strategic technology guidance.