Lesson 4 of 6
Custom logic with event handlers
9 min
Listen to this lesson
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.
Before, on and after
| Phase | What it is for | How handlers run | Fleet app example |
|---|---|---|---|
| before | Validate and enrich input; reject early | All registered before handlers run; any error fails the request | Reject closeInquiry without a resolution |
| on | Do the actual work, instead of or in addition to the generic handler | An interceptor stack: the top handler runs first and may delegate down to the generic default with next() | Implement the closeInquiry action |
| after | Post-process results | Receive the result set; all registered after handlers run | Add a virtual highPriority flag to each row read |
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.
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.
- Is it a required field, a range, a format or an existing reference?Yes → @mandatory or @assert annotation · No → continue
- Is it about who may do what?Yes → @requires or @restrict (course sap-btp-204) · No → continue
- Does it check or enrich input before saving?Yes → before handler · No → continue
- Is it a custom operation or a call to another system?Yes → on handler · No → continue
- Does it adjust what is returned?Yes → after handler
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?
- 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. Which handler phase is meant for validating input and rejecting early?
2. What does req.reject() do in a CAP Node.js handler?
3. Where does CAP look for the implementation of srv/fleet-service.cds by default?
4. Why use CQL queries instead of hand-written SQL strings in handlers?