23 · Custom Service Handlers

Write custom handlers with this.on(), this.before(), this.after(). Understand the handler chain and next().

Episode 23 — Custom Service Handlers

What you'll learn

Why this matters

The generic service provider covers 90 % of CRUD with zero code. The remaining 10 % — business rules, side-effects, external system calls — needs custom handlers. Knowing exactly how to hook in without breaking the generic behaviour is the core skill of CAP service development.

The request lifecycle (quick review)

[HTTP / OData request]
       ↓
  BEFORE phase   ← validate, enrich input
       ↓
  ON phase       ← execute the operation  ← generic handler lives here
       ↓
  AFTER phase    ← enrich output, side-effects
       ↓
[HTTP / OData response]

Every phase can have multiple registered handlers; they run in registration order.

Registering your first handler

// srv/cat-service.js
const cds = require('@sap/cds')

module.exports = class CatalogService extends cds.ApplicationService {
  async init () {

    // this.on  → replaces the default handler for an event
    this.on('READ', 'Books', async (req) => {
      const books = await SELECT.from('Books')
      return books
    })

    // IMPORTANT: always call super.init() LAST
    await super.init()
  }
}

Rule: await super.init() wires up the generic handlers. Call it after your registrations so your handlers run first, or the generic one fires before yours for ON events.

Accessing request data

this.on('CREATE', 'Orders', async (req) => {
  const { data } = req          // payload from the request body
  const { user }  = req         // authenticated user (req.user.id, req.user.roles)
  const { query } = req         // CQL query object
  const tenant    = req.tenant  // tenant ID in multitenant apps

  console.log('Creating order for', user.id, 'data:', data)

  // delegate to generic handler after custom logic
  return this.emit('CREATE', 'Orders', req)   // or: return next()
})

Returning data

Return value Effect
An array / object Sent directly as the response
undefined / nothing Continues to the next handler
req.reject(...) Throws an HTTP error and stops the chain
next() Passes control to the next registered handler

Handler scope patterns

// All events on an entity
this.on(['CREATE','UPDATE'], 'Books', handler)

// Any entity (wildcard)
this.on('CREATE', '*', handler)

// Bound action defined in CDS
this.on('submitOrder', 'Orders', async (req) => { /* ... */ })

// Unbound action
this.on('pingSystem', async (req) => { /* ... */ })

next() — chaining handlers

this.on('READ', 'Books', async (req, next) => {
  console.log('Before generic READ')
  const result = await next()   // run the next handler (generic in this case)
  console.log('After generic READ, got', result.length, 'rows')
  return result
})

Project structure for handlers

srv/
├── cat-service.cds          ← service definition
├── cat-service.js           ← main handlers (auto-loaded by name match)
└── lib/
    ├── orders-handler.js    ← imported manually if needed
    └── validation.js

CAP auto-loads srv/foo-service.js when srv/foo-service.cds defines FooService. For complex services, split handlers across files and import them in init().

// srv/cat-service.js
const { registerOrderHandlers } = require('./lib/orders-handler')

module.exports = class CatalogService extends cds.ApplicationService {
  async init () {
    registerOrderHandlers(this)   // pass `this` = the service instance
    await super.init()
  }
}

Common mistakes

Mistake Symptom Fix
Forgetting await super.init() Generic handlers don't run Always call it
Calling super.init() before registrations Your handler runs after generic Move registrations before super.init()
Returning undefined from this.on() Request hangs Return data or call next()
this.on('read', ...) (lowercase) Handler silently ignored Use uppercase: 'READ'

Hands-on exercise

  1. Scaffold cds init bookshop && cd bookshop && cds add sample
  2. Create srv/cat-service.js extending cds.ApplicationService
  3. Add a this.before('CREATE', 'Orders', ...) that sets req.data.createdBy = req.user.id
  4. Add a this.after('READ', 'Books', ...) that adds inStock: true to books with stock > 0
  5. Run cds watch and verify with a REST client

Checkpoint ✓

Sign in for progress tracking & Q&A →