SAP Cloud Application Programming Model (CAP)
Full-stack framework for SAP BTP — CDS modeling, Node.js services, Fiori UIs.
Prerequisites
- Node.js ≥ 18 and npm ≥ 7
@sap/cds-dkinstalled globally:npm i -g @sap/cds-dk- Cloud Foundry CLI (
cf) for deployment - MBT build tool:
npm i -g mbt
1. Create Project
cds init my-cap-app --add sample,hana
cd my-cap-app
npm install
Project layout:
my-cap-app/
├── db/schema.cds # Data model
├── db/data/ # CSV seed data
├── srv/service.cds # Service definitions
├── srv/service.js # Custom handlers
├── app/ # Fiori Elements UIs + annotations
├── mta.yaml # MTA descriptor (cds add mta)
└── package.json
2. Define Data Model (db/schema.cds)
namespace sap.capire.risks;
entity Risks : cuid, managed {
title : String(100);
prio : String(5);
impact : Integer;
criticality : Integer;
miti : Composition of many Mitigations on miti.risk = $self;
}
entity Mitigations : cuid, managed {
description : String;
owner : String;
risk : Association to Risks;
}
3. Define Service (srv/risk-service.cds)
using sap.capire.risks from '../db/schema';
service RiskService @(requires: 'authenticated-user') {
entity Risks as projection on risks.Risks;
entity Mitigations as projection on risks.Mitigations;
action analyzeRisks() returns Risks;
}
4. Implement Custom Handler (srv/risk-service.js)
const cds = require('@sap/cds')
module.exports = class RiskService extends cds.ApplicationService {
async init() {
const { Risks } = this.entities
// Validate before create
this.before('CREATE', Risks, (req) => {
req.data.criticality = Math.ceil(req.data.impact / 20) * 20
})
// Custom action
this.on('analyzeRisks', async (req) => {
return UPDATE(req.subject).with({ title: { '=': req.data.title } })
})
// After-read enrichment
this.after('READ', Risks, (rows) => {
rows.forEach(r => { if (r.impact > 80) r.criticality = 1 })
})
return super.init()
}
}
5. Add Fiori Annotations (app/risks/annotations.cds)
using RiskService as service from '../../srv/risk-service';
annotate service.Risks with @(
UI: {
SelectionFields: [ title, prio ],
LineItem: [
{ $Type: 'UI.DataField', Value: title },
{ $Type: 'UI.DataField', Value: prio },
{ $Type: 'UI.DataField', Value: criticality, Criticality: criticality }
],
HeaderInfo: {
TypeName: 'Risk', TypeNamePlural: 'Risks',
Title: { Value: title }
}
}
);
6. Run Locally
cds watch # SQLite, auto-reload on file change
# Visit http://localhost:4004
7. Hybrid Testing (local server + BTP HANA/XSUAA)
cds add hana,xsuaa,mta --for production
cf deploy mta_archives/risks_1.0.0.mtar # deploy once to create services
cds bind -2 my-hdi,my-xsuaa # bind local dev to BTP services
cds watch --profile hybrid # local runtime + cloud backends
8. Deploy to Cloud Foundry
cds add mta # generate mta.yaml if not present
mbt build -t gen/mta.tar
cf deploy gen/mta.tar
# Or simplified: cds up (builds + deploys in one step)
Pitfalls
cds rundeprecated — Cause: cds 8+ removed thecdsexecutable from@sap/cds. Solution: usecds-servein npm scripts:npm pkg set scripts.start="cds-serve".Cross-tenant data leak — Cause: caching request-scoped data in module-level variables. Solution: use
cds.contextor keep state inside the handler closure.XSUAA scopes not updated — Cause: changing
@requiresannotations doesn't auto-updatexs-security.json. Solution: re-runcds compile --to xsuaaafter auth annotation changes.cuidadds UUID key — Cause::cuidaspect auto-generates aString(36)UUID key. Solution: don't manually definekey IDwhen usingcuid; it's already included.@readonlyblocks writes — Cause:@readonlyrestricts entity to GET only. Solution: remove annotation or use@insertonly/@updatablefor partial access.Hybrid profile missing bindings — Cause:
cds bindsaves to.cdsrc-private.jsonwhich isn't committed. Solution: re-runcds bind -2 <service>after cloning the repo.
Verification
# 1. Check model compiles
cds compile srv/risk-service.cds -2 edmx
# 2. Verify local server responds
curl http://localhost:4004/odata/v4/risk/Risks?$top=5
# 3. Check service endpoints
cds eval 'cds.served["RiskService"].endpoints'
# 4. Validate MTA descriptor
mbt build -t gen/mta.tar && cf deploy gen/mta.tar --dry-run
# 5. Confirm Fiori annotations render
# Open http://localhost:4004/risks/webapp/index.html