Write custom handlers with this.on(), this.before(), this.after(). Understand the handler chain and next().
this.on(), this.before(), this.after()next() to chain handlersThe generic service provider covers 90 % of CRUD with zero code. The remaining 10 % — business rules, side-effects, external system calls — needs custom handlers. Knowing exactly how to hook in without breaking the generic behaviour is the core skill of CAP service development.
[HTTP / OData request]
↓
BEFORE phase ← validate, enrich input
↓
ON phase ← execute the operation ← generic handler lives here
↓
AFTER phase ← enrich output, side-effects
↓
[HTTP / OData response]
Every phase can have multiple registered handlers; they run in registration order.
// srv/cat-service.js
const cds = require('@sap/cds')
module.exports = class CatalogService extends cds.ApplicationService {
async init () {
// this.on → replaces the default handler for an event
this.on('READ', 'Books', async (req) => {
const books = await SELECT.from('Books')
return books
})
// IMPORTANT: always call super.init() LAST
await super.init()
}
}
Rule:
await super.init()wires up the generic handlers. Call it after your registrations so your handlers run first, or the generic one fires before yours for ON events.
this.on('CREATE', 'Orders', async (req) => {
const { data } = req // payload from the request body
const { user } = req // authenticated user (req.user.id, req.user.roles)
const { query } = req // CQL query object
const tenant = req.tenant // tenant ID in multitenant apps
console.log('Creating order for', user.id, 'data:', data)
// delegate to generic handler after custom logic
return this.emit('CREATE', 'Orders', req) // or: return next()
})
| Return value | Effect |
|---|---|
| An array / object | Sent directly as the response |
undefined / nothing |
Continues to the next handler |
req.reject(...) |
Throws an HTTP error and stops the chain |
next() |
Passes control to the next registered handler |
// All events on an entity
this.on(['CREATE','UPDATE'], 'Books', handler)
// Any entity (wildcard)
this.on('CREATE', '*', handler)
// Bound action defined in CDS
this.on('submitOrder', 'Orders', async (req) => { /* ... */ })
// Unbound action
this.on('pingSystem', async (req) => { /* ... */ })
this.on('READ', 'Books', async (req, next) => {
console.log('Before generic READ')
const result = await next() // run the next handler (generic in this case)
console.log('After generic READ, got', result.length, 'rows')
return result
})
srv/
├── cat-service.cds ← service definition
├── cat-service.js ← main handlers (auto-loaded by name match)
└── lib/
├── orders-handler.js ← imported manually if needed
└── validation.js
CAP auto-loads srv/foo-service.js when srv/foo-service.cds defines FooService. For complex services, split handlers across files and import them in init().
// srv/cat-service.js
const { registerOrderHandlers } = require('./lib/orders-handler')
module.exports = class CatalogService extends cds.ApplicationService {
async init () {
registerOrderHandlers(this) // pass `this` = the service instance
await super.init()
}
}
| Mistake | Symptom | Fix |
|---|---|---|
Forgetting await super.init() |
Generic handlers don't run | Always call it |
Calling super.init() before registrations |
Your handler runs after generic | Move registrations before super.init() |
Returning undefined from this.on() |
Request hangs | Return data or call next() |
this.on('read', ...) (lowercase) |
Handler silently ignored | Use uppercase: 'READ' |
cds init bookshop && cd bookshop && cds add samplesrv/cat-service.js extending cds.ApplicationServicethis.before('CREATE', 'Orders', ...) that sets req.data.createdBy = req.user.idthis.after('READ', 'Books', ...) that adds inStock: true to books with stock > 0cds watch and verify with a REST clientbefore, on, and after handlersnext() vs return dataawait super.init() is always your last init line