| name | service-handlers |
| description | Use when implementing SAP CAP service handlers: service.js, service.ts, ApplicationService, before/on/after, event handler, custom action, custom function, cds.ql, SELECT INSERT UPDATE DELETE, srv.run, CRUD implementation, CAP Node.js business logic, handler registration.
|
| metadata | {"category":"cap","version":"1.0.0","keywords":["CAP","service","handler","before","on","after","ApplicationService","init","super.init","cds.ql","SELECT","INSERT","UPDATE","DELETE","action","function","srv"],"related":{"cds-modeling":"define the CDS entities the handlers operate on","error-handling":"reject or warn inside handlers with proper error codes","security-auth":"check req.user and roles inside handlers","testing":"write integration tests for service handlers","performance":"avoid N+1 queries inside handlers"}} |
Service Handlers — CAP Best Practices
Primary reference: https://cap.cloud.sap/docs/node.js/core-services
Event handlers: https://cap.cloud.sap/docs/node.js/events
Handler registration patterns
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()
}
async validateProduct(req) {
if (!req.data.title) req.reject(400, 'Title is required')
}
async onSubmitOrder(req) {
const { product, quantity } = req.data
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
const products = await SELECT.from(Products)
.where({ category_ID: req.data.categoryId })
.columns('ID', 'title', 'price')
.orderBy('price desc')
.limit(20)
const order = await SELECT.one.from(Orders, req.params[0])
.columns(o => { o`.*`, o.items`.*` })
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 via req.data in handlers — don't rely on the return value.
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]
const [newProduct] = await INSERT.into(Products).entries({ title: 'New Product', price: 29.99, currency_code: 'EUR' })
const { affected } = await UPDATE(Products, id).with({ price: 24.99 })
await UPSERT.into(Products).entries(data)
const { affected: gone } = .().({ : id })
Using srv.run() for cross-service calls
const db = await cds.connect.to('db')
const AdminSrv = await cds.connect.to('AdminService')
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
}
this.on('submitOrder', async (req) => {
const { orderID } = req.data
return true
})
this.on('getStatus', async (req) => {
const order = await SELECT.one(Orders, req.data.orderID)
return order?.status ?? 'UNKNOWN'
})
Error handling in handlers
req.reject(422, 'Quantity must be positive')
req.reject(409, 'ORDER_ALREADY_CLOSED', [req.data.orderID])
req.warn(200, 'Stock is low for product {0}', [product.title])
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
Common mistakes to avoid
- ❌ Forgetting
await super.init() in class-based handlers
- ❌ Using
req.query directly instead of the CQL fluent API
- ❌ Doing DB reads inside
after handlers in a loop (N+1 queries)
- ❌ Missing
return in on handlers (returns undefined → empty response)
- ❌ Mutating
req.data inside after handlers (it's the result, not the input there)
- ❌ Using
cds.db.run() directly when srv.run() or await query pattern suffices