Skip to content
Vladimir Chavkov
Go back

SAP Cloud Application Programming Model (CAP): Complete Development Guide

Edit page

SAP Cloud Application Programming Model (CAP): Complete Development Guide

The SAP Cloud Application Programming Model (CAP) is a framework of languages, libraries, and tools for building enterprise-grade services and applications. This comprehensive guide covers everything you need to build production-ready applications with CAP.

What is SAP CAP?

CAP is SAP’s opinionated framework for building cloud-native applications that:

Why Choose CAP?

Traditional Development vs CAP Development:

// Traditional approach (100+ lines)
app.get('/api/books', authenticate, authorize, async (req, res) => {
const connection = await pool.getConnection();
const query = `SELECT * FROM books WHERE deleted = 0`;
const results = await connection.execute(query);
// Handle pagination, filtering, sorting...
// Handle errors...
// Handle caching...
// Handle audit logging...
res.json(results);
});
// CAP approach (declarative)
service CatalogService {
entity Books as projection on my.Books;
}
// Everything else is handled automatically!

Core Concepts

1. Core Data Services (CDS)

CDS is a universal modeling language for defining data models and service APIs:

// schema.cds - Domain model
namespace bookshop;
entity Books {
key ID : UUID;
title : String(111);
descr : String(1111);
author : Association to Authors;
stock : Integer;
price : Decimal(9,2);
currency : Currency;
}
entity Authors {
key ID : UUID;
name : String(111);
books : Association to many Books on books.author = $self;
}
// Common types and aspects
type Currency : String(3);
aspect managed {
createdAt : Timestamp @cds.on.insert: $now;
createdBy : String @cds.on.insert: $user;
modifiedAt : Timestamp @cds.on.insert: $now @cds.on.update: $now;
modifiedBy : String @cds.on.insert: $user @cds.on.update: $user;
}

2. Service Definitions

Define OData services that expose your entities:

srv/cat-service.cds
using { bookshop } from '../db/schema';
service CatalogService @(path:'/browse') {
@readonly entity Books as projection on bookshop.Books {
*, author.name as authorName
} excluding { createdBy, modifiedBy };
@requires: 'authenticated-user'
entity Authors as projection on bookshop.Authors;
@requires: 'Admin'
action submitOrder (book: Books:ID, quantity: Integer);
function getRecommendations(genre: String) returns array of Books;
}

3. Custom Event Handlers

Implement business logic in Node.js or Java:

// srv/cat-service.js - Node.js implementation
const cds = require('@sap/cds');
module.exports = cds.service.impl(async function() {
const { Books, Authors } = this.entities;
// Before READ - add computed values
this.after('READ', 'Books', (books) => {
books.forEach(book => {
if (book.stock > 100) book.availability = 'In Stock';
else if (book.stock > 0) book.availability = 'Low Stock';
else book.availability = 'Out of Stock';
});
});
// Before CREATE - validation
this.before('CREATE', 'Books', (req) => {
const { price, stock } = req.data;
if (price < 0) req.error(400, 'Price must be positive');
if (stock < 0) req.error(400, 'Stock cannot be negative');
});
// Custom action
this.on('submitOrder', async (req) => {
const { book, quantity } = req.data;
// Check stock
const bookRecord = await SELECT.one.from(Books).where({ ID: book });
if (!bookRecord) return req.error(404, 'Book not found');
if (bookRecord.stock < quantity) {
return req.error(409, 'Insufficient stock');
}
// Update stock
await UPDATE(Books)
.set({ stock: bookRecord.stock - quantity })
.where({ ID: book });
// Emit event for other services
await this.emit('OrderPlaced', {
book: book,
quantity: quantity,
customer: req.user.id
});
return { success: true, orderNumber: generateOrderNumber() };
});
// Custom function
this.on('getRecommendations', async (req) => {
const { genre } = req.data;
// Use CDS Query Language (CQL)
const recommendations = await SELECT.from(Books)
.where({ genre: genre })
.orderBy('rating desc')
.limit(10);
return recommendations;
});
// Integration with external service
const S4Sales = await cds.connect.to('S4SalesService');
this.after('READ', 'Books', async (books, req) => {
// Enhance with S/4HANA data
for (const book of books) {
if (book.ISBN) {
const salesData = await S4Sales.run(
SELECT.from('ProductSalesData')
.where({ ProductID: book.ISBN })
);
book.totalSales = salesData?.TotalSales || 0;
}
}
});
});
function generateOrderNumber() {
return `ORD-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}

Java Implementation

srv/src/main/java/com/bookshop/handlers/CatalogServiceHandler.java
package com.bookshop.handlers;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.*;
import com.sap.cds.services.cds.CdsService;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.Update;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnUpdate;
import com.sap.cds.Result;
import org.springframework.stereotype.Component;
import cds.gen.catalogservice.*;
import cds.gen.bookshop.*;
import java.math.BigDecimal;
import java.util.List;
@Component
@ServiceName(CatalogService_.CDS_NAME)
public class CatalogServiceHandler implements EventHandler {
@Before(event = CdsService.EVENT_CREATE, entity = Books_.CDS_NAME)
public void validateBookCreate(CreateContext context, Books book) {
if (book.getPrice() != null &&
book.getPrice().compareTo(BigDecimal.ZERO) < 0) {
context.getMessages().error("Price must be positive");
context.setCompleted();
}
}
@After(event = CdsService.EVENT_READ, entity = Books_.CDS_NAME)
public void calculateAvailability(List<Books> books) {
books.forEach(book -> {
Integer stock = book.getStock();
if (stock != null) {
if (stock > 100) book.setAvailability("In Stock");
else if (stock > 0) book.setAvailability("Low Stock");
else book.setAvailability("Out of Stock");
}
});
}
@On(event = "submitOrder")
public void handleSubmitOrder(SubmitOrderContext context) {
String bookId = context.getBook();
Integer quantity = context.getQuantity();
// Query book
CqnSelect select = Select.from(Books_.class).where(b -> b.ID().eq(bookId));
Books book = context.getService().run(select).single(Books.class);
if (book == null) {
context.getMessages().error("Book not found");
context.setCompleted();
return;
}
if (book.getStock() < quantity) {
context.getMessages().error("Insufficient stock");
context.setCompleted();
return;
}
// Update stock
CqnUpdate update = Update.entity(Books_.class)
.data(Books.create().setStock(book.getStock() - quantity))
.where(b -> b.ID().eq(bookId));
context.getService().run(update);
// Emit event
OrderPlacedEvent event = OrderPlacedEvent.create();
event.setBook(bookId);
event.setQuantity(quantity);
context.getService().emit(event);
context.setResult("Order submitted successfully");
}
}

Advanced Features

1. Aspect-Oriented Programming with Aspects

// Common aspects for reuse
aspect managed {
createdAt : Timestamp @cds.on.insert: $now;
createdBy : User @cds.on.insert: $user;
modifiedAt : Timestamp @cds.on.insert: $now @cds.on.update: $now;
modifiedBy : User @cds.on.insert: $user @cds.on.update: $user;
}
aspect temporal {
validFrom : Date @cds.on.insert: $now;
validTo : Date default '9999-12-31';
}
aspect cuid {
key ID : UUID;
}
// Use aspects in entities
entity Orders : cuid, managed, temporal {
orderNumber : String(20) @mandatory;
customer : Association to Customers;
items : Composition of many OrderItems on items.order = $self;
totalAmount : Decimal(15,2);
status : String(20) @assert.range: ['New', 'Processing', 'Shipped', 'Delivered'];
}
entity OrderItems : cuid {
order : Association to Orders;
product : Association to Products;
quantity : Integer @assert.range: [1, 9999];
price : Decimal(15,2);
}

2. Authorization and Access Control

srv/admin-service.cds
service AdminService @(requires: 'Admin') {
entity Books as projection on bookshop.Books;
entity Authors as projection on bookshop.Authors;
// Field-level access control
@restrict: [
{ grant: 'READ', to: 'Admin' },
{ grant: ['CREATE', 'UPDATE'], to: 'Admin',
where: 'createdBy = $user' }
]
entity Orders as projection on bookshop.Orders;
}
// Instance-based authorization
@restrict: [
{
grant: 'READ',
to: 'authenticated-user',
where: 'createdBy = $user.id'
},
{
grant: '*',
to: 'Admin'
}
]
entity PersonalData {
key ID : UUID;
userId : String;
sensitiveData : String;
createdBy : String;
}

3. Fiori Draft Support

Enable draft mode for edit sessions:

service CatalogService {
@odata.draft.enabled
entity Books as projection on bookshop.Books;
}
// Draft is handled automatically, but you can add custom logic
this.before('SAVE', 'Books', async (req) => {
// Custom validation before activating draft
const book = req.data;
if (!book.ISBN) {
req.error(400, 'ISBN is required before publishing');
}
// Validate ISBN format
if (!/^\d{10}(\d{3})?$/.test(book.ISBN)) {
req.error(400, 'Invalid ISBN format');
}
});
this.on('CANCEL', 'Books', async (req) => {
// Cleanup when draft is canceled
console.log(`Draft ${req.data.ID} was canceled by ${req.user.id}`);
});

4. Multitenancy

srv/server.js
const cds = require('@sap/cds');
cds.on('served', () => {
const { Books } = cds.entities('bookshop');
// Tenant-specific initialization
cds.on('tenant-subscribe', async (tenant, metadata) => {
console.log(`New tenant subscribed: ${tenant}`);
// Initialize tenant-specific data
await cds.tx({ tenant }, async (tx) => {
await tx.run(
INSERT.into(Books).entries([
{ title: 'Welcome Book', stock: 10, price: 9.99 }
])
);
});
return 'Subscription successful';
});
cds.on('tenant-unsubscribe', async (tenant) => {
console.log(`Tenant unsubscribed: ${tenant}`);
// Cleanup tenant data if needed
});
});
module.exports = cds.server;

5. Integration with External Services

// External service definition
using { API_BUSINESS_PARTNER as bp } from '../srv/external/API_BUSINESS_PARTNER';
service IntegrationService {
entity Customers as projection on bp.A_BusinessPartner {
BusinessPartner as ID,
BusinessPartnerName as name,
BusinessPartnerCategory as category
};
entity LocalOrders as projection on bookshop.Orders {
*,
customer.name as customerName
};
}
srv/integration-service.js
const cds = require('@sap/cds');
module.exports = async function() {
const bupa = await cds.connect.to('API_BUSINESS_PARTNER');
const { Customers, LocalOrders } = this.entities;
// Delegate to external service
this.on('READ', 'Customers', async (req) => {
return bupa.run(req.query);
});
// Hybrid query - local + remote
this.after('READ', 'LocalOrders', async (orders) => {
const customerIds = orders.map(o => o.customerID);
// Fetch from S/4HANA
const customers = await bupa.run(
SELECT.from('A_BusinessPartner')
.where({ BusinessPartner: { in: customerIds } })
);
const customerMap = new Map(
customers.map(c => [c.BusinessPartner, c])
);
// Enrich orders with S/4HANA data
orders.forEach(order => {
const customer = customerMap.get(order.customerID);
if (customer) {
order.customerName = customer.BusinessPartnerName;
order.customerCategory = customer.BusinessPartnerCategory;
}
});
});
// Create with external system
this.before('CREATE', 'LocalOrders', async (req) => {
const { customerID } = req.data;
// Verify customer exists in S/4HANA
const customer = await bupa.run(
SELECT.one.from('A_BusinessPartner')
.where({ BusinessPartner: customerID })
);
if (!customer) {
req.error(404, `Customer ${customerID} not found in S/4HANA`);
}
});
};

Database Integration

SAP HANA Cloud Artifacts

-- db/src/procedures/GetTopBooks.hdbprocedure
PROCEDURE "GetTopBooks"(
IN limit INTEGER,
OUT results TABLE (
ID NVARCHAR(36),
title NVARCHAR(111),
totalSales INTEGER,
revenue DECIMAL(15,2)
)
)
LANGUAGE SQLSCRIPT
SQL SECURITY INVOKER
AS
BEGIN
results = SELECT
b.ID,
b.title,
COUNT(oi.ID) as totalSales,
SUM(oi.quantity * oi.price) as revenue
FROM Books b
LEFT JOIN OrderItems oi ON oi.book_ID = b.ID
GROUP BY b.ID, b.title
ORDER BY totalSales DESC
LIMIT :limit;
END;
// Call HANA procedure from CAP
this.on('getTopBooks', async (req) => {
const { limit = 10 } = req.data;
const db = await cds.connect.to('db');
const result = await db.run(
`CALL "GetTopBooks"(${limit}, ?)`
);
return result;
});

Calculation Views

<!-- db/src/views/BookSalesView.hdbcalculationview -->
<?xml version="1.0" encoding="UTF-8"?>
<Calculation:scenario xmlns:Calculation="http://www.sap.com/ndb/BiModelCalculation.ecore">
<calculationViews>
<calculationView xsi:type="Calculation:ProjectionView" id="Books">
<datasources>
<DataSource id="BOOKS">
<resourceUri>BOOKS</resourceUri>
</DataSource>
</datasources>
<projectionNode>
<mapping xsi:type="Calculation:AttributeMapping" target="ID" source="ID"/>
<mapping xsi:type="Calculation:AttributeMapping" target="TITLE" source="TITLE"/>
<mapping xsi:type="Calculation:AttributeMapping" target="PRICE" source="PRICE"/>
</projectionNode>
</calculationView>
</calculationViews>
</Calculation:scenario>

Testing

Unit Tests

test/cat-service.test.js
const cds = require('@sap/cds/lib');
const { expect } = require('chai');
describe('Catalog Service', () => {
let srv, Books;
before(async () => {
srv = await cds.connect.to('CatalogService');
Books = srv.entities.Books;
});
it('should return all books', async () => {
const books = await srv.read(Books);
expect(books).to.be.an('array');
expect(books.length).to.be.greaterThan(0);
});
it('should create a new book', async () => {
const newBook = {
title: 'Test Book',
author_ID: 'some-author-id',
stock: 10,
price: 29.99
};
const result = await srv.create(Books).entries(newBook);
expect(result).to.have.property('ID');
});
it('should reject negative price', async () => {
try {
await srv.create(Books).entries({
title: 'Invalid Book',
price: -10
});
expect.fail('Should have thrown error');
} catch (err) {
expect(err.message).to.include('Price must be positive');
}
});
it('should submit order successfully', async () => {
const books = await srv.read(Books).limit(1);
const book = books[0];
const result = await srv.send({
method: 'POST',
path: '/submitOrder',
data: {
book: book.ID,
quantity: 2
}
});
expect(result).to.have.property('success', true);
expect(result).to.have.property('orderNumber');
});
});

Integration Tests

test/integration/api.test.js
const cds = require('@sap/cds/lib');
const { POST, GET, DELETE } = cds.test(__dirname + '/../..');
describe('API Integration Tests', () => {
it('should authenticate and access protected endpoint', async () => {
const { data, status } = await GET('/browse/Books', {
auth: { username: 'alice', password: '' }
});
expect(status).to.equal(200);
expect(data.value).to.be.an('array');
});
it('should require authentication', async () => {
const { status } = await GET('/browse/Authors');
expect(status).to.equal(401);
});
it('should enforce authorization', async () => {
const { status } = await POST('/browse/Books', {
title: 'Unauthorized Book'
}, {
auth: { username: 'reader', password: '' }
});
expect(status).to.equal(403);
});
});

Performance Optimization

1. Database Query Optimization

// Bad - N+1 query problem
this.after('READ', 'Orders', async (orders) => {
for (const order of orders) {
order.customer = await SELECT.one.from(Customers)
.where({ ID: order.customer_ID });
}
});
// Good - Single query with JOIN
this.on('READ', 'Orders', async (req, next) => {
req.query.SELECT.columns('*', {
ref: ['customer'],
expand: ['*']
});
return next();
});

2. Caching

const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes
this.on('READ', 'Books', async (req, next) => {
const cacheKey = `books_${JSON.stringify(req.query)}`;
// Check cache
const cached = cache.get(cacheKey);
if (cached) return cached;
// Execute query
const result = await next();
// Store in cache
cache.set(cacheKey, result);
return result;
});

3. Streaming Large Results

this.on('READ', 'LargeDataset', async (req) => {
const stream = await SELECT.from('LargeDataset')
.where(req.query.SELECT.where)
.stream();
return stream;
});

Production Deployment

1. Configuration Management

package.json
{
"cds": {
"requires": {
"db": {
"kind": "sql",
"[production]": {
"kind": "hana",
"credentials": {}
}
},
"auth": {
"[production]": {
"kind": "xsuaa"
}
}
},
"log": {
"levels": {
"[production]": {
"app": "info",
"db": "warn"
}
}
}
}
}

2. Health Checks

srv/health.js
module.exports = (app) => {
app.get('/health', async (req, res) => {
const health = {
status: 'UP',
timestamp: new Date().toISOString(),
checks: {}
};
try {
// Check database
const db = await cds.connect.to('db');
await db.run('SELECT 1 FROM DUMMY');
health.checks.database = 'UP';
} catch (err) {
health.checks.database = 'DOWN';
health.status = 'DOWN';
}
const statusCode = health.status === 'UP' ? 200 : 503;
res.status(statusCode).json(health);
});
};

Conclusion

SAP CAP dramatically simplifies enterprise application development by providing a comprehensive framework with best practices built-in. From data modeling with CDS to automatic OData API generation, from built-in authentication to seamless HANA integration, CAP enables developers to focus on business logic rather than boilerplate code.

The framework’s flexibility supports both Node.js and Java, making it accessible to a wide range of development teams while maintaining consistency in approach and capabilities.


Master SAP CAP development with our comprehensive SAP BTP training programs. Learn from hands-on labs and real-world scenarios. Contact us for customized enterprise training.


Edit page
Share this post on:

Previous Post
SAP Gardener: Enterprise Kubernetes Management at Scale
Next Post
SAP Business Technology Platform (BTP): Complete Guide for Enterprise Development