The CAPstone Project: A Journey from Component to Cloud

Estimated read time 15 min read

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

You May Also Like

More From Author