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.
In practice. A Leave Management app validates leave dates in a before handler — the end date can't precede the start date, overlapping leave is rejected, and the day count is auto-calculated:
this.before(['CREATE', 'UPDATE'], [LeaveRequests, LeaveRequests.drafts], async (req) => {
const { startDate, endDate, employee_ID, ID } = req.data;
if (startDate && endDate) {
if (endDate < startDate) {
req.error(400, 'End date cannot be before start date.');
}
const msPerDay = 24 * 60 * 60 * 1000;
req.data.days = Math.round((new Date(endDate) - new Date(startDate)) / msPerDay) + 1;
}
if (employee_ID && startDate && endDate) {
const overlapping = await SELECT.from(LeaveRequests).where({
employee_ID,
status: { in: ['Pending', 'Approved'] },
startDate: { '<=': endDate },
endDate: { '>=': startDate },
...(ID ? { ID: { '!=': ID } } : {})
});
if (overlapping.length > 0) {
req.error(400, 'This employee already has a pending or approved leave in this period.');
}
}
});
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.
In practice. Manager approval is a bound action — an on handler that flips the request status:
async function setStatus(req, status) {
const { ID } = req.params[0];
await UPDATE(req.target).set({ status }).where({ ID });
return status === 'Approved' ? 'Leave request approved.' : 'Leave request rejected.';
}
this.on('approve', [LeaveRequests, LeaveRequests.drafts], (req) => setStatus(req, 'Approved'));
this.on('reject', [LeaveRequests, LeaveRequests.drafts], (req) => setStatus(req, 'Rejected'));
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.