Introduction
In this post, I’ll take you through complete journey: of building React + Vite(Component) application on SAP’s ecosystem(Cloud).
Someone, who is React developer and curious about SAP’s environment or a CAP expert just exploring modern frontend option, this blog will give them a practical and real-world solution with code snippets.
Let’s dive how modern JavaScript development can thrive alongside enterprise SAP capabilities.
Open BAS
Create a CAP Project
cds init project_name
npm install
Create a basic schema schema.cds
namespace UserManagement;
using { managed, cuid } from ‘@sap/cds/common’;
entity Users : managed, cuid {
name : String(100) not null;
email : String(255) not null;
company : String(100) not null;
}
Service in service.cds
using UserManagement from ‘../db/schema’;
service UserService {
entity Users as projection on UserManagement.Users;
function getUser() returns array of {
name : String(100) not null;
email : String(255) not null;
company : String(100) not null;
}
}
Custom service service.js
const cds = require(‘@sap/cds’);
const { message } = require(‘@sap/cds/lib/log/cds-error’);
module.exports = cds.service.impl(async function() {
const { Users } = this.entities;
// Before CREATE event – validate data
this.before(‘CREATE’, ‘Users’, async (req) => {
const { name, email, company } = req.data;
// Basic validation
if (!name || !email || !company) {
req.reject(400, ‘All fields (name, email, company) are required’);
}
// Email validation
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
if (!emailRegex.test(email)) {
req.reject(400, ‘Invalid email format’);
}
// Check for duplicate email
const existingUser = await SELECT.one.from(Users).where({ email: email });
if (existingUser) {
req.reject(400, ‘User with this email already exists’);
}
});
// After CREATE event – log the action
this.after(‘CREATE’, ‘Users’, async (data, req) => {
console.log(`New user created: ${data.name} (${data.email})`);
});
this.on(‘getUser’,async(req)=>{
const data = await SELECT.from(‘Users’);
return {status:200,message:’Data Fetched’,data}
})
});
Add csv file for mock data
cds add data
Add the below mock data
ID,name,email,company,createdAt,modifiedAt
1,John Doe,john.doe@example.com,Tech Corp,2024-01-15T10:00:00Z,2024-01-15T10:00:00Z
2,Jane Smith,jane.smith@innovate.io,Innovate Solutions,2024-01-16T11:30:00Z,2024-01-16T11:30:00Z
3,Mike Johnson,mike.j@startup.com,StartupX,2024-01-17T09:15:00Z,2024-01-17T09:15:00Z
Now let’s Create React Project
cd app
npm create vite@latest .
As shown below
Install Lucide React for icon
npm install lucide-react
Install Tailwind CSS(optional)
Setup Proxy Server to avoid CORS issue
vite.config.js
import { defineConfig } from ‘vite’
import react from ‘@vitejs/plugin-react’
import tailwindcss from ‘@tailwindcss/vite’
// https://vite.dev/config/
export default defineConfig({
plugins: [react(),tailwindcss()],
server:{
proxy:{
‘/odata’:{
target:’http://localhost:42745′,
changeOrigin:true,
rewrite: path=>path.replace(‘/^/odata/’,’/odata’),
}
}
}
})
Create the UI
App.jsx
import React, { useState } from ‘react’;
import { Plus, Database, User, Mail, Building } from ‘lucide-react’;
import ‘./app.css’
const App = () => {
const [formData, setFormData] = useState({
name: ”,
email: ”,
company: ”
});
const [users, setUsers] = useState([]);
const [showTable, setShowTable] = useState(false);
const [loading, setLoading] = useState(false);
// Nord color palette
const nordColors = {
polarNight: [‘#2e3440’, ‘#3b4252’, ‘#434c5e’, ‘#4c566a’],
snowStorm: [‘#d8dee9’, ‘#e5e9f0’, ‘#eceff4’],
frost: [‘#8fbcbb’, ‘#88c0d0’, ‘#81a1c1’, ‘#5e81ac’],
aurora: [‘#bf616a’, ‘#d08770’, ‘#ebcb8b’, ‘#a3be8c’, ‘#b48ead’]
};
const handleInputChange = (e) => {
setFormData({
…formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async () => {
if (!formData.name || !formData.email || !formData.company) {
alert(‘Please fill in all fields’);
return;
}
setLoading(true);
try {
// Simulate API call to CAP backend
const response = await fetch(‘odata/v4/user/Users’, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
},
body: JSON.stringify(formData)
});
if (response.ok) {
// For demo purposes, add to local state
const newUser = {
id: Date.now(),
…formData,
createdAt: new Date().toISOString()
};
setUsers(prev => […prev, newUser]);
setFormData({ name: ”, email: ”, company: ” });
alert(‘User saved successfully!’);
}
} catch (error) {
// For demo, still add to local state
const newUser = {
id: Date.now(),
…formData,
createdAt: new Date().toISOString()
};
setUsers(prev => […prev, newUser]);
setFormData({ name: ”, email: ”, company: ” });
alert(‘User saved successfully! (Demo mode)’);
}
setLoading(false);
};
const fetchUsers = async () => {
setLoading(true);
setShowTable(true);
try {
// Simulate API call to fetch users
const response = await fetch(‘odata/v4/user/getUser’);
if (response.ok) {
const data = await response.json();
console.log(‘log on ui’,data.value[0].data)
setUsers(data.value[0].data);
}
} catch (error) {
// Demo mode – use local state
console.log(‘Using demo data’);
}
setLoading(false);
};
return (
<div className=”min-h-screen” style={{ backgroundColor: nordColors.polarNight[0] }}>
<div className=”container mx-auto px-4 py-8″>
{/* Header */}
<div className=”text-center mb-8″>
<h1 className=”text-4xl font-bold mb-2″ style={{ color: nordColors.snowStorm[2] }}>
User Management
</h1>
<p className=”text-lg” style={{ color: nordColors.snowStorm[0] }}>
Simple form with SAP BTP CAP backend
</p>
</div>
<div className=”grid grid-cols-1 lg:grid-cols-2 gap-8″>
{/* Form Section */}
<div
className=”p-6 rounded-lg shadow-xl”
style={{ backgroundColor: nordColors.polarNight[1] }}
>
<h2
className=”text-2xl font-semibold mb-6 flex items-center gap-2″
style={{ color: nordColors.frost[1] }}
>
<Plus className=”w-6 h-6″ />
Add New User
</h2>
<div className=”space-y-4″>
<div>
<label
className=”block text-sm font-medium mb-2 flex items-center gap-2″
style={{ color: nordColors.snowStorm[1] }}
>
<User className=”w-4 h-4″ />
Name
</label>
<input
type=”text”
name=”name”
value={formData.name}
onChange={handleInputChange}
required
className=”w-full px-4 py-2 rounded-md border focus:ring-2 focus:outline-none transition-all”
style={{
backgroundColor: nordColors.polarNight[2],
borderColor: nordColors.polarNight[3],
color: nordColors.snowStorm[2],
focusRingColor: nordColors.frost[2]
}}
placeholder=”Enter full name”
/>
</div>
<div>
<label
className=”block text-sm font-medium mb-2 flex items-center gap-2″
style={{ color: nordColors.snowStorm[1] }}
>
<Mail className=”w-4 h-4″ />
Email
</label>
<input
type=”email”
name=”email”
value={formData.email}
onChange={handleInputChange}
required
className=”w-full px-4 py-2 rounded-md border focus:ring-2 focus:outline-none transition-all”
style={{
backgroundColor: nordColors.polarNight[2],
borderColor: nordColors.polarNight[3],
color: nordColors.snowStorm[2]
}}
placeholder=”Enter email address”
/>
</div>
<div>
<label
className=”block text-sm font-medium mb-2 flex items-center gap-2″
style={{ color: nordColors.snowStorm[1] }}
>
<Building className=”w-4 h-4″ />
Company
</label>
<input
type=”text”
name=”company”
value={formData.company}
onChange={handleInputChange}
required
className=”w-full px-4 py-2 rounded-md border focus:ring-2 focus:outline-none transition-all”
style={{
backgroundColor: nordColors.polarNight[2],
borderColor: nordColors.polarNight[3],
color: nordColors.snowStorm[2]
}}
placeholder=”Enter company name”
/>
</div>
<button
type=”button”
onClick={handleSubmit}
disabled={loading}
className=”w-full py-3 px-4 rounded-md font-semibold transition-all hover:opacity-90 disabled:opacity-50″
style={{
backgroundColor: nordColors.aurora[3],
color: nordColors.polarNight[0]
}}
>
{loading ? ‘Saving…’ : ‘Save User’}
</button>
</div>
</div>
{/* Data Display Section */}
<div
className=”p-6 rounded-lg shadow-xl”
style={{ backgroundColor: nordColors.polarNight[1] }}
>
<h2
className=”text-2xl font-semibold mb-6 flex items-center gap-2″
style={{ color: nordColors.frost[1] }}
>
<Database className=”w-6 h-6″ />
User Database
</h2>
<button
onClick={fetchUsers}
disabled={loading}
className=”mb-6 py-2 px-4 rounded-md font-semibold transition-all hover:opacity-90 disabled:opacity-50″
style={{
backgroundColor: nordColors.frost[2],
color: nordColors.snowStorm[2]
}}
>
{loading ? ‘Loading…’ : ‘Show All Users’}
</button>
{showTable && (
<div className=”overflow-x-auto”>
{users.length === 0 ? (
<p style={{ color: nordColors.snowStorm[0] }}>
No users found. Add some users using the form.
</p>
) : (
<table className=”w-full”>
<thead>
<tr style={{ borderBottom: `2px solid ${nordColors.polarNight[3]}` }}>
<th
className=”text-left py-3 px-2 font-semibold”
style={{ color: nordColors.frost[1] }}
>
Name
</th>
<th
className=”text-left py-3 px-2 font-semibold”
style={{ color: nordColors.frost[1] }}
>
Email
</th>
<th
className=”text-left py-3 px-2 font-semibold”
style={{ color: nordColors.frost[1] }}
>
Company
</th>
</tr>
</thead>
<tbody>
{users.map((user, index) => (
<tr
key={user.ID}
className=”hover:opacity-80 transition-opacity”
style={{
borderBottom: `1px solid ${nordColors.polarNight[2]}`,
backgroundColor: index % 2 === 0 ? nordColors.polarNight[0] : ‘transparent’
}}
>
<td
className=”py-3 px-2″
style={{ color: nordColors.snowStorm[1] }}
>
{user.name}
</td>
<td
className=”py-3 px-2″
style={{ color: nordColors.snowStorm[1] }}
>
{user.email}
</td>
<td
className=”py-3 px-2″
style={{ color: nordColors.snowStorm[1] }}
>
{user.company}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default App;
App.css
@import “tailwindcss”;
The Landing Page
Click on show all Users to see all data
Insert data in Form and Save User data
Data added
Conclusion
Our journey in this blog proved that React+Vite and SAP CAP can work together beautifully – but success requires understanding the nuances of integration, security, and deployment in the BTP environment,
My approach was to integrate with external APIs and services beyond the SAP ecosystem. For purely internal, form-heavy applications, UI5 might still be the more better choice.
Please feel free to ask any questions you may have.
IntroductionIn this post, I’ll take you through complete journey: of building React + Vite(Component) application on SAP’s ecosystem(Cloud).Someone, who is React developer and curious about SAP’s environment or a CAP expert just exploring modern frontend option, this blog will give them a practical and real-world solution with code snippets.Let’s dive how modern JavaScript development can thrive alongside enterprise SAP capabilities.Open BAS Create a CAP Project cds init project_name
npm install Create a basic schema schema.cds namespace UserManagement;
using { managed, cuid } from ‘@sap/cds/common’;
entity Users : managed, cuid {
name : String(100) not null;
email : String(255) not null;
company : String(100) not null;
} Service in service.cds using UserManagement from ‘../db/schema’;
service UserService {
entity Users as projection on UserManagement.Users;
function getUser() returns array of {
name : String(100) not null;
email : String(255) not null;
company : String(100) not null;
}
} Custom service service.js const cds = require(‘@sap/cds’);
const { message } = require(‘@sap/cds/lib/log/cds-error’);
module.exports = cds.service.impl(async function() {
const { Users } = this.entities;
// Before CREATE event – validate data
this.before(‘CREATE’, ‘Users’, async (req) => {
const { name, email, company } = req.data;
// Basic validation
if (!name || !email || !company) {
req.reject(400, ‘All fields (name, email, company) are required’);
}
// Email validation
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
if (!emailRegex.test(email)) {
req.reject(400, ‘Invalid email format’);
}
// Check for duplicate email
const existingUser = await SELECT.one.from(Users).where({ email: email });
if (existingUser) {
req.reject(400, ‘User with this email already exists’);
}
});
// After CREATE event – log the action
this.after(‘CREATE’, ‘Users’, async (data, req) => {
console.log(`New user created: ${data.name} (${data.email})`);
});
this.on(‘getUser’,async(req)=>{
const data = await SELECT.from(‘Users’);
return {status:200,message:’Data Fetched’,data}
})
}); Add csv file for mock data cds add data Add the below mock data ID,name,email,company,createdAt,modifiedAt
1,John Doe,john.doe@example.com,Tech Corp,2024-01-15T10:00:00Z,2024-01-15T10:00:00Z
2,Jane Smith,jane.smith@innovate.io,Innovate Solutions,2024-01-16T11:30:00Z,2024-01-16T11:30:00Z
3,Mike Johnson,mike.j@startup.com,StartupX,2024-01-17T09:15:00Z,2024-01-17T09:15:00Z Now let’s Create React Project cd app
npm create vite@latest . As shown belowInstall Lucide React for icon npm install lucide-react Install Tailwind CSS(optional)Setup Proxy Server to avoid CORS issuevite.config.js import { defineConfig } from ‘vite’
import react from ‘@vitejs/plugin-react’
import tailwindcss from ‘@tailwindcss/vite’
// https://vite.dev/config/
export default defineConfig({
plugins: [react(),tailwindcss()],
server:{
proxy:{
‘/odata’:{
target:’http://localhost:42745′,
changeOrigin:true,
rewrite: path=>path.replace(‘/^/odata/’,’/odata’),
}
}
}
}) Create the UIApp.jsx import React, { useState } from ‘react’;
import { Plus, Database, User, Mail, Building } from ‘lucide-react’;
import ‘./app.css’
const App = () => {
const [formData, setFormData] = useState({
name: ”,
email: ”,
company: ”
});
const [users, setUsers] = useState([]);
const [showTable, setShowTable] = useState(false);
const [loading, setLoading] = useState(false);
// Nord color palette
const nordColors = {
polarNight: [‘#2e3440’, ‘#3b4252’, ‘#434c5e’, ‘#4c566a’],
snowStorm: [‘#d8dee9’, ‘#e5e9f0’, ‘#eceff4’],
frost: [‘#8fbcbb’, ‘#88c0d0’, ‘#81a1c1’, ‘#5e81ac’],
aurora: [‘#bf616a’, ‘#d08770’, ‘#ebcb8b’, ‘#a3be8c’, ‘#b48ead’]
};
const handleInputChange = (e) => {
setFormData({
…formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async () => {
if (!formData.name || !formData.email || !formData.company) {
alert(‘Please fill in all fields’);
return;
}
setLoading(true);
try {
// Simulate API call to CAP backend
const response = await fetch(‘odata/v4/user/Users’, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
},
body: JSON.stringify(formData)
});
if (response.ok) {
// For demo purposes, add to local state
const newUser = {
id: Date.now(),
…formData,
createdAt: new Date().toISOString()
};
setUsers(prev => […prev, newUser]);
setFormData({ name: ”, email: ”, company: ” });
alert(‘User saved successfully!’);
}
} catch (error) {
// For demo, still add to local state
const newUser = {
id: Date.now(),
…formData,
createdAt: new Date().toISOString()
};
setUsers(prev => […prev, newUser]);
setFormData({ name: ”, email: ”, company: ” });
alert(‘User saved successfully! (Demo mode)’);
}
setLoading(false);
};
const fetchUsers = async () => {
setLoading(true);
setShowTable(true);
try {
// Simulate API call to fetch users
const response = await fetch(‘odata/v4/user/getUser’);
if (response.ok) {
const data = await response.json();
console.log(‘log on ui’,data.value[0].data)
setUsers(data.value[0].data);
}
} catch (error) {
// Demo mode – use local state
console.log(‘Using demo data’);
}
setLoading(false);
};
return (
<div className=”min-h-screen” style={{ backgroundColor: nordColors.polarNight[0] }}>
<div className=”container mx-auto px-4 py-8″>
{/* Header */}
<div className=”text-center mb-8″>
<h1 className=”text-4xl font-bold mb-2″ style={{ color: nordColors.snowStorm[2] }}>
User Management
</h1>
<p className=”text-lg” style={{ color: nordColors.snowStorm[0] }}>
Simple form with SAP BTP CAP backend
</p>
</div>
<div className=”grid grid-cols-1 lg:grid-cols-2 gap-8″>
{/* Form Section */}
<div
className=”p-6 rounded-lg shadow-xl”
style={{ backgroundColor: nordColors.polarNight[1] }}
>
<h2
className=”text-2xl font-semibold mb-6 flex items-center gap-2″
style={{ color: nordColors.frost[1] }}
>
<Plus className=”w-6 h-6″ />
Add New User
</h2>
<div className=”space-y-4″>
<div>
<label
className=”block text-sm font-medium mb-2 flex items-center gap-2″
style={{ color: nordColors.snowStorm[1] }}
>
<User className=”w-4 h-4″ />
Name
</label>
<input
type=”text”
name=”name”
value={formData.name}
onChange={handleInputChange}
required
className=”w-full px-4 py-2 rounded-md border focus:ring-2 focus:outline-none transition-all”
style={{
backgroundColor: nordColors.polarNight[2],
borderColor: nordColors.polarNight[3],
color: nordColors.snowStorm[2],
focusRingColor: nordColors.frost[2]
}}
placeholder=”Enter full name”
/>
</div>
<div>
<label
className=”block text-sm font-medium mb-2 flex items-center gap-2″
style={{ color: nordColors.snowStorm[1] }}
>
<Mail className=”w-4 h-4″ />
Email
</label>
<input
type=”email”
name=”email”
value={formData.email}
onChange={handleInputChange}
required
className=”w-full px-4 py-2 rounded-md border focus:ring-2 focus:outline-none transition-all”
style={{
backgroundColor: nordColors.polarNight[2],
borderColor: nordColors.polarNight[3],
color: nordColors.snowStorm[2]
}}
placeholder=”Enter email address”
/>
</div>
<div>
<label
className=”block text-sm font-medium mb-2 flex items-center gap-2″
style={{ color: nordColors.snowStorm[1] }}
>
<Building className=”w-4 h-4″ />
Company
</label>
<input
type=”text”
name=”company”
value={formData.company}
onChange={handleInputChange}
required
className=”w-full px-4 py-2 rounded-md border focus:ring-2 focus:outline-none transition-all”
style={{
backgroundColor: nordColors.polarNight[2],
borderColor: nordColors.polarNight[3],
color: nordColors.snowStorm[2]
}}
placeholder=”Enter company name”
/>
</div>
<button
type=”button”
onClick={handleSubmit}
disabled={loading}
className=”w-full py-3 px-4 rounded-md font-semibold transition-all hover:opacity-90 disabled:opacity-50″
style={{
backgroundColor: nordColors.aurora[3],
color: nordColors.polarNight[0]
}}
>
{loading ? ‘Saving…’ : ‘Save User’}
</button>
</div>
</div>
{/* Data Display Section */}
<div
className=”p-6 rounded-lg shadow-xl”
style={{ backgroundColor: nordColors.polarNight[1] }}
>
<h2
className=”text-2xl font-semibold mb-6 flex items-center gap-2″
style={{ color: nordColors.frost[1] }}
>
<Database className=”w-6 h-6″ />
User Database
</h2>
<button
onClick={fetchUsers}
disabled={loading}
className=”mb-6 py-2 px-4 rounded-md font-semibold transition-all hover:opacity-90 disabled:opacity-50″
style={{
backgroundColor: nordColors.frost[2],
color: nordColors.snowStorm[2]
}}
>
{loading ? ‘Loading…’ : ‘Show All Users’}
</button>
{showTable && (
<div className=”overflow-x-auto”>
{users.length === 0 ? (
<p style={{ color: nordColors.snowStorm[0] }}>
No users found. Add some users using the form.
</p>
) : (
<table className=”w-full”>
<thead>
<tr style={{ borderBottom: `2px solid ${nordColors.polarNight[3]}` }}>
<th
className=”text-left py-3 px-2 font-semibold”
style={{ color: nordColors.frost[1] }}
>
Name
</th>
<th
className=”text-left py-3 px-2 font-semibold”
style={{ color: nordColors.frost[1] }}
>
Email
</th>
<th
className=”text-left py-3 px-2 font-semibold”
style={{ color: nordColors.frost[1] }}
>
Company
</th>
</tr>
</thead>
<tbody>
{users.map((user, index) => (
<tr
key={user.ID}
className=”hover:opacity-80 transition-opacity”
style={{
borderBottom: `1px solid ${nordColors.polarNight[2]}`,
backgroundColor: index % 2 === 0 ? nordColors.polarNight[0] : ‘transparent’
}}
>
<td
className=”py-3 px-2″
style={{ color: nordColors.snowStorm[1] }}
>
{user.name}
</td>
<td
className=”py-3 px-2″
style={{ color: nordColors.snowStorm[1] }}
>
{user.email}
</td>
<td
className=”py-3 px-2″
style={{ color: nordColors.snowStorm[1] }}
>
{user.company}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default App; App.css @import “tailwindcss”; The Landing Page Click on show all Users to see all dataInsert data in Form and Save User dataData addedConclusionOur journey in this blog proved that React+Vite and SAP CAP can work together beautifully – but success requires understanding the nuances of integration, security, and deployment in the BTP environment,My approach was to integrate with external APIs and services beyond the SAP ecosystem. For purely internal, form-heavy applications, UI5 might still be the more better choice.Please feel free to ask any questions you may have. Read More Technology Blog Posts by Members articles
#SAP
#SAPTechnologyblog