The request lifecycle in three phases
Every CAP request flows through three handler phases: before, on, and after. Understanding which phase does what is the single most important concept for writing CAP backend logic.
beforevalidates and enriches.onimplements.aftertransforms the result. Memorize that sentence.
before: validate and enrich input
this.before('CREATE', 'Orders', async (req) => {
const { quantity } = req.data;
if (quantity <= 0) {
req.error(400, 'Quantity must be positive');
}
req.data.createdBy = req.user.id;
});
before handlers run before the database operation. Use them for input validation (req.error aborts the request) and for stamping data the client shouldn't set.
on: provide the implementation
this.on('READ', 'Books', async (req) => {
// custom read logic, e.g. from an external source
return fetchBooksFromCatalog(req.query);
});
Registering an on handler replaces the default implementation. If you don't register one, CAP's generic handler does the database work for you. Most handlers you'll write are before and after — the defaults handle the middle.
Only write
onhandlers when the default behavior is wrong — external data sources, computed results, special persistence.
after: shape the output
this.after('READ', 'Orders', (orders) => {
orders.forEach(o => {
o.totalAmount = o.items.reduce((s, i) => s + i.price * i.quantity, 0);
});
});
after handlers receive the result and can enrich or reshape it. Computed fields, formatting, and adding virtual properties belong here.
Handler ordering and specificity
- More specific wins —
this.before('CREATE', 'Orders')runs beforethis.before('CREATE', '*'). - Multiple handlers chain — several
beforehandlers on the same event all run, in registration order. req.errorvsreq.reject—erroradds a message and continues collecting;rejectaborts immediately.- Async everywhere — handlers can be async; CAP awaits them in order.
Common handler patterns
Defaulting: set values the client didn't provide in before CREATE — status fields, timestamps, generated numbers. The client sends business data; the handler completes it.
Cascading: in after CREATE on a parent, create related child records. Keep it in the same transaction so partial failures roll back cleanly.
Guarding: in before UPDATE, compare req.data against the current record and reject illegal transitions — e.g. changing an order's customer after approval.
Handlers should be small and single-purpose. A 200-line
before UPDATEis a design smell — split it into focused handlers.
Put logic in the right phase
Validation in before, defaults in before, custom persistence in on, computed output in after. When logic feels awkward, it's usually in the wrong phase.
Confused about a handler scenario? Lay it out in the comments.