Now accepting guest editorial from SAP, Oracle, Workday and ServiceNow practitioners.Pitch a guide
The Sign-Off Table

Lesson 4 of 6

Custom logic with event handlers

9 min

Listen to this lesson

0:00 / 5:43

Some rules cannot be declared: an inquiry may only be closed with a resolution text, closing must stamp who closed it, and the fleet desk wants inquiries on high-priority orders flagged. CAP handles such logic with event handlers. Every request is an event (READ, CREATE, UPDATE, DELETE, or an action name) on a target entity, and you register functions that run before, on or after it.

The implementation file

CAP finds the implementation by convention: a .js file with the same name next to the .cds file, so srv/fleet-service.js implements srv/fleet-service.cds. The class-based style extends cds.ApplicationService, registers handlers in init() and calls super.init() at the end:
const cds = require('@sap/cds')
module.exports = class FleetService extends cds.ApplicationService {
init() {
const { Inquiries } = this.entities
this.before('closeInquiry', req => { if (!req.data.resolution) req.reject(400, 'A resolution is required to close an inquiry') })
this.on('closeInquiry', async req => { await UPDATE(Inquiries, req.data.ID).with({ status: 'Closed' }) })
this.after('READ', Inquiries, rows => { /* flag high-priority rows */ })
return super.init()
}
}
This is the CommonJS form found in most existing projects. Projects created with cds 10 are ES modules by default; there the file starts with import cds from '@sap/cds' and exports the same class with export default.

How the closeInquiry handlers in srv/fleet-service.js share the work: the before handler rejects bad input, and the on handler does the update.
The Sign-Off Table · thesignofftable.com/academy/sap-btp-202

Before, on and after

PhaseWhat it is forHow handlers runFleet app example
beforeValidate and enrich input; reject earlyAll registered before handlers run; any error fails the requestReject closeInquiry without a resolution
onDo the actual work, instead of or in addition to the generic handlerAn interceptor stack: the top handler runs first and may delegate down to the generic default with next()Implement the closeInquiry action
afterPost-process resultsReceive the result set; all registered after handlers runAdd a virtual highPriority flag to each row read
The three handler phases, as described in CAP's documentation.
The Sign-Off Table · thesignofftable.com/academy/sap-btp-202

Inside a handler, req.data holds the payload or action parameters, req.user the current user, and req.target the entity. To fail a request, req.reject() throws an error that CAP sends to the client as an error response; req.error() collects errors and CAP rejects the request after the current phase if any were recorded, which is useful when you want to report several problems at once. Use meaningful HTTP status codes (400 for bad input, 403 for not allowed, 404 for not found, 409 for a conflicting state) because UI messages and tests rely on them.

req.reject stops the request at once, while req.error collects messages and CAP rejects the request after the phase if any were recorded.
The Sign-Off Table · thesignofftable.com/academy/sap-btp-202

Rules for maintainable handlers

  • Declare before you code. Use @mandatory, @assert and authorization annotations first; write handlers only for what they cannot express.
  • Keep handlers small. One rule per handler, named or commented by its business rule, so a failing test points to one place.
  • Use CQL, not string SQL. Queries such as SELECT.from(Inquiries).where(...) stay database-agnostic between SQLite and SAP HANA Cloud.
  • Do not swallow errors. Let CAP turn rejections into proper error responses; log context, not personal data.
  • Stay agnostic. Call other services through cds.connect.to rather than raw HTTP so they can be mocked in tests.
  1. Is it a required field, a range, a format or an existing reference?Yes → @mandatory or @assert annotation · No → continue
  2. Is it about who may do what?Yes → @requires or @restrict (course sap-btp-204) · No → continue
  3. Does it check or enrich input before saving?Yes → before handler · No → continue
  4. Is it a custom operation or a call to another system?Yes → on handler · No → continue
  5. Does it adjust what is returned?Yes → after handler
Where a rule belongs in a CAP service.
The Sign-Off Table · thesignofftable.com/academy/sap-btp-202

What would you do?

Dan asks that every closed inquiry record who closed it and when. A developer suggests adding closedBy and closedAt fields and filling them in the UI before sending the request. What should the reviewer recommend?

Key points
  • Every request is an event on a target; register before, on and after handlers for it.
  • Implement services in a same-named .js file, extending cds.ApplicationService and calling super.init().
  • req.reject throws immediately; req.error collects errors that CAP raises after the phase.
  • Declare what you can, code what you must, and keep each handler to one rule.
  • Set audit-relevant values on the server from req.user and server time, never from the client.

Check your understanding · not graded

Lesson check

  1. 1. Which handler phase is meant for validating input and rejecting early?

  2. 2. What does req.reject() do in a CAP Node.js handler?

  3. 3. Where does CAP look for the implementation of srv/fleet-service.cds by default?

  4. 4. Why use CQL queries instead of hand-written SQL strings in handlers?