CAP's built-in generic service providers — what is handled for you and when you need custom handlers.
What CAP's generic service providers handle automatically, how deep insert and deep read work, what you need to implement yourself, and the exact lifecycle of a CAP request from HTTP to database.
When you define a service and run cds watch, CAP automatically handles:
GET /Books, GET /Books(id), filtering, sorting, pagination, expandPOST /Books with validationPUT /Books(id) and PATCH /Books(id)DELETE /Books(id)@mandatory, @assert.range, enumsAll of this runs without a single line of handler code from you.
Every incoming request goes through this pipeline:
HTTP Request
│
▼
BEFORE handlers ← your pre-processing hooks (validation, enrichment)
│
▼
ON handlers ← your business logic (replaces default behavior)
│ If no ON handler: CAP's generic handler runs here
▼
AFTER handlers ← your post-processing hooks (transform, enrich response)
│
▼
HTTP Response
If you define an ON handler, CAP's generic handler does NOT run. Your handler must call next() if you want both to execute.
// CAP does all of this for you automatically:
// 1. Parse the OData URL ($filter, $select, $expand, etc.)
// 2. Translate to SQL:
// SELECT ID, title, price FROM Books
// WHERE price < 20
// ORDER BY title ASC
// LIMIT 10 OFFSET 0
// 3. Execute against SQLite/HANA
// 4. Format as OData JSON response
// 5. Handle $expand (additional JOINs or sub-SELECTs)
// 6. Add @odata.count if $count=true
Given this POST body:
{
"customerName": "Alice",
"items": [
{ "product": "Book A", "qty": 2 },
{ "product": "Book B", "qty": 1 }
]
}
CAP automatically:
1. Validates required fields
2. Generates UUIDs for all entities
3. INSERTs the Order record
4. INSERTs each OrderItem, linking to the parent Order ID
5. Returns the created tree
This works because the items field is defined as a Composition in CDS.
If your entity uses the managed aspect:
using { managed } from '@sap/cds/common';
entity Books : managed {
key ID: UUID;
title : String;
}
CAP automatically sets on CREATE:
- createdAt = current timestamp
- createdBy = current user (from JWT / mock auth)
And on UPDATE:
- modifiedAt = current timestamp
- modifiedBy = current user
You never set these manually — CAP handles them.
If you declare key ID : UUID, CAP generates a UUID for new records automatically. Clients do not need to supply the ID on POST.
POST /odata/v4/catalog/Books
Content-Type: application/json
{ "title": "New Book", "price": 25.00 }
Response:
{
"ID": "a1b2c3d4-...", ← auto-generated by CAP
"title": "New Book",
"price": 25.00,
"createdAt": "2024-01-15T10:30:00Z",
"createdBy": "alice"
}
You need a custom handler when:
| Scenario | Example |
|---|---|
| Business logic on write | Recalculate inventory when an order is placed |
| Computed fields at read time | Add a virtual discount based on customer tier |
| External system calls | Create a record in S/4HANA when an order is created |
| Complex validation | Check stock before creating an order |
| Side effects | Send an email when a user registers |
| Custom auth logic | Restrict which rows a user can see based on their attributes |
Create srv/catalog-service.js (same base name as catalog-service.cds):
const cds = require('@sap/cds');
module.exports = class CatalogService extends cds.ApplicationService {
async init() {
// BEFORE READ — add a log
this.before('READ', 'Books', req => {
console.log(`User ${req.user.id} is reading Books`);
});
// AFTER READ — add a virtual field
this.after('READ', 'Books', books => {
for (const book of books) {
book.inStock = book.stock > 0;
}
});
// ON CREATE — validate before save
this.before('CREATE', 'Books', req => {
if (req.data.price < 0) {
req.error(400, 'Price cannot be negative');
}
});
// Call super.init() to register CAP's generic handlers
await super.init();
}
}
Critical: always call
await super.init()at the end of yourinit()method. This registers CAP's generic handlers for operations you didn't override.
BEFORE handlers (yours, in registration order)
│
▼
ON handlers (yours, OR CAP's generic if you didn't register one)
│
▼
AFTER handlers (yours, in registration order)
Multiple before handlers stack — all run in order. Multiple on handlers — only the first one registered runs (unless it calls next()).
super.init()// ❌ WRONG — generic CRUD broken, nothing works
module.exports = class MyService extends cds.ApplicationService {
async init() {
this.before('READ', 'Books', req => { /* ... */ });
// forgot super.init() — CAP's generic READ handler never registered!
}
}
// ✅ CORRECT
module.exports = class MyService extends cds.ApplicationService {
async init() {
this.before('READ', 'Books', req => { /* ... */ });
await super.init(); // always last
}
}
managed adds automatically?Module 1 complete! You now have a solid foundation in CAP. In Module 2, we go deep into CDS data modeling — associations, compositions, aspects, views, annotations, and everything you need to model real-world business data.
Sign in for progress tracking & Q&A →