24 · Before & After Phases

Use BEFORE for validation/enrichment, AFTER for side effects. Implement cross-cutting concerns without modifying entity handlers.

Episode 24 — Before / After / On Hooks in Depth

What you'll learn

Why this matters

CAP's three-phase model maps directly to enterprise patterns: input validation (BEFORE), business logic (ON), and post-processing / side-effects (AFTER). Knowing which hook to use prevents subtle bugs where you validate data that's already been saved, or enrich a response that's already been sent.

Phase semantics

BEFORE

Purpose:   Validate or enrich the incoming request BEFORE execution.
req.data:  The payload as sent by the client (mutable — you can change it here).
req.query: The CQL query built from the OData URL.
Effect:    Call req.reject() to abort. Modify req.data to change what's saved.
this.before('CREATE', 'Books', (req) => {
  if (!req.data.title) return req.reject(400, 'Title is required')
  req.data.createdAt = new Date().toISOString()   // enrich before save
})

ON

Purpose:   Execute the operation (read from DB, write to DB, call external).
req.data:  Same as BEFORE; the generic handler uses it to build SQL.
Return:    Your return value becomes the response. Call next() to use generic.
this.on('READ', 'Analytics', async (req) => {
  // Completely replace with custom logic
  return db.run(`SELECT department, SUM(salary) AS total FROM Employees GROUP BY department`)
})

AFTER

Purpose:   Enrich or log the result AFTER execution.
data:      The result data (second argument). Mutable for READ results.
req.data:  The original request payload (still available).
Return:    Ignored for READ — mutate the array/object in place instead.
this.after('READ', 'Books', (books, req) => {
  books.forEach(b => {
    b.priceFormatted = `€ ${b.price.toFixed(2)}`
    b.inStock = b.stock > 0
  })
  // No return needed — mutating the array is sufficient
})

Execution order for multiple handlers

// Handlers fire in registration order within each phase
this.before('READ', 'Books', authCheck)        // fires first
this.before('READ', 'Books', tenantFilter)     // fires second

this.on('READ', 'Books', next => next())       // defers to generic (fires first)
// generic READ handler wired by super.init()   // fires second (inside generic slot)

this.after('READ', 'Books', addComputedFields) // fires first
this.after('READ', 'Books', logAccess)         // fires second

Cross-cutting patterns

Audit logging (AFTER)

this.after(['CREATE','UPDATE','DELETE'], '*', (data, req) => {
  auditLog.write({
    entity:    req.target.name,
    operation: req.event,
    user:      req.user.id,
    key:       req.data?.ID,
    at:        new Date()
  })
})

Request enrichment (BEFORE)

this.before('*', '*', (req) => {
  req.data = req.data || {}
  req.data._requestId = crypto.randomUUID()   // attach trace ID
})

Response transformation (AFTER)

this.after('READ', 'Products', (products) => {
  products.forEach(p => {
    delete p._internal_costBasis   // strip internal fields
  })
})

Short-circuiting the chain

this.before('DELETE', 'Orders', async (req) => {
  const order = await SELECT.one.from('Orders').where({ ID: req.data.ID })
  if (order.status === 'SHIPPED') {
    return req.reject(409, `Order ${req.data.ID} already shipped — cannot delete`)
    // Returning req.reject() stops ALL subsequent handlers (before, on, after)
  }
})

this.reject() vs req.reject()

req.reject(status, msg) this.reject(status, msg)
Returns A rejected promise (throw-like) Same
Use in Handler function body Outside handler (rare)
Best practice Use req.reject() inside handlers Avoid outside

Testing hooks in isolation

// test/books.test.js
const cds = require('@sap/cds/lib')

describe('Books BEFORE handler', () => {
  const { GET, POST } = cds.test('.').in(__dirname + '/..')

  it('rejects create without title', async () => {
    const res = await POST('/catalog/Books', { stock: 5 })
    expect(res.status).toBe(400)
  })
})

Hands-on exercise

  1. Add this.before('UPDATE', 'Books', ...) — reject if req.data.price < 0
  2. Add this.after('READ', 'Books', ...) — add discountedPrice = price * 0.9 to all books
  3. Add this.after(['CREATE','UPDATE'], 'Books', ...) — log the operation to console
  4. Verify the order: put console.log in each hook and confirm sequence

Checkpoint ✓

Sign in for progress tracking & Q&A →