Primary reference: https://cap.cloud.sap/docs/node.js/core-services Event handlers: https://cap.cloud.sap/docs/node.js/events
Handler registration patterns
// Preferred: class-based handler (collocate logic, testable)
module.exports = class CatalogService extends cds.ApplicationService {
async init() {
const { Products } = this.entities
this.before('CREATE', Products, this.validateProduct)
this.on('submitOrder', this.onSubmitOrder)
this.after('READ', Products, this.enrichProducts)
return super.init() // always call super.init()!
}
async validateProduct(req) { if (!req.data.title) req.reject(400, 'Title is required') }
async onSubmitOrder(req) { const { product, quantity } = req.data // ... business logic return { success: true } }
async enrichProducts(products, req) { for (const p of products) p._isExpensive = p.price > 1000 } }
## Handler phases
| Phase | When to use |
|-------|------------|
| `before` | Validation, authorization checks, data enrichment before DB access |
| `on` | Replace the default DB operation entirely (custom actions/functions, overrides) |
| `after` | Enrich/transform results, trigger side effects after DB write |
## Reading data with cds.ql
```js
// Fluent query API — always prefer over raw SQL
const products = await SELECT.from(Products)
.where({ category_ID: req.data.categoryId })
.columns('ID', 'title', 'price')
.orderBy('price desc')
.limit(20)
// With associations (expand)
const order = await SELECT.one.from(Orders, req.params[0])
.columns(o => { o`.*`, o.items`.*` })
// Shorthand for single record
const product = await SELECT.one(Products, id)
Writing data
CDS 10+: Write operations return a uniform result object. INSERT returns an iterable array with generated keys; UPDATE/DELETE return
{ affected }. Access input viareq.datain handlers — don't rely on the return value.
// INSERT — result is iterable with generated keys
const result = await INSERT.into(Products).entries(
{ title: 'Book 1', price: 29.99, currency_code: 'EUR' },
{ title: 'Book 2', price: 19.99, currency_code: 'EUR' }
)
const [book1, book2] = [...result] // generated IDs
// Single INSERT
const [newProduct] = await INSERT.into(Products).entries({ title: 'New Product', price: 29.99, currency_code: 'EUR' })
// UPDATE — returns { affected: number }
const { affected } = await UPDATE(Products, id).with({ price: 24.99 })
// UPSERT (CAP 7+)
await UPSERT.into(Products).entries(data)
// DELETE — returns { affected: number }
const { affected: gone } = await DELETE.from(Products).where({ ID: id })
Using srv.run() for cross-service calls
// Call another service from within a handler
const db = await cds.connect.to('db')
const AdminSrv = await cds.connect.to('AdminService')
// Run multiple operations in the same transaction
await cds.run([
UPDATE(Stock).set({ quantity: q.quantity - req.data.amount }).where({ product_ID: id }),
INSERT.into(AuditLog).entries({ action: 'ORDER', ... })
])
Custom actions and functions
// In .cds:
service OrderService {
action submitOrder(orderID: UUID) returns Boolean; // action = modifies data
function getStatus(orderID: UUID) returns String; // function = read-only
}
// In handler:
this.on('submitOrder', async (req) => {
const { orderID } = req.data
// ... process
return true
})
this.on('getStatus', async (req) => {
const order = await SELECT.one(Orders, req.data.orderID)
return order?.status ?? 'UNKNOWN'
})
Error handling in handlers
// Reject with HTTP status (stops processing, sends error response)
req.reject(422, 'Quantity must be positive')
// Error with i18n key
req.reject(409, 'ORDER_ALREADY_CLOSED', [req.data.orderID])
// Warn (adds to @SAP.Messages, processing continues)
req.warn(200, 'Stock is low for product {0}', [product.title])
// Programmatic error
throw new cds.error('Something went wrong', { status: 500 })
Accessing the user and locale
const { user, locale, tenant } = req
const isAdmin = user.is('admin')
const userName = user.id // e.g. 'alice@example.com'
Common mistakes to avoid
- ❌ Forgetting
await super.init()in class-based handlers - ❌ Using
req.querydirectly instead of the CQL fluent API - ❌ Doing DB reads inside
afterhandlers in a loop (N+1 queries) - ❌ Missing
returninonhandlers (returns undefined → empty response) - ❌ Mutating
req.datainsideafterhandlers (it's the result, not the input there) - ❌ Using
cds.db.run()directly whensrv.run()or await query pattern suffices