SuperAPI AI Directory

service-handlers

Coding & development

View repository

Updated 10-16 10:47

About this skill

Service Handlers — CAP Best Practices

Primary reference: https://cap.cloud.sap/docs/node.js/core-services Event handlers: https://cap.cloud.sap/docs/node.js/events

Handler registration patterns

// Preferred: class-based handler (collocate logic, testable)
module.exports = class CatalogService extends cds.ApplicationService {
  async init() {
    const { Products } = this.entities

    this.before('CREATE', Products, this.validateProduct)
    this.on('submitOrder', this.onSubmitOrder)
    this.after('READ', Products, this.enrichProducts)

    return super.init()  // always call super.init()!
  }

async validateProduct(req) { if (!req.data.title) req.reject(400, 'Title is required') }

async onSubmitOrder(req) { const { product, quantity } = req.data // ... business logic return { success: true } }

async enrichProducts(products, req) { for (const p of products) p._isExpensive = p.price > 1000 } }


## Handler phases

| Phase | When to use |
|-------|------------|
| `before` | Validation, authorization checks, data enrichment before DB access |
| `on` | Replace the default DB operation entirely (custom actions/functions, overrides) |
| `after` | Enrich/transform results, trigger side effects after DB write |

## Reading data with cds.ql

```js
// Fluent query API — always prefer over raw SQL
const products = await SELECT.from(Products)
  .where({ category_ID: req.data.categoryId })
  .columns('ID', 'title', 'price')
  .orderBy('price desc')
  .limit(20)

// With associations (expand)
const order = await SELECT.one.from(Orders, req.params[0])
  .columns(o => { o`.*`, o.items`.*` })

// Shorthand for single record
const product = await SELECT.one(Products, id)

Writing data

CDS 10+: Write operations return a uniform result object. INSERT returns an iterable array with generated keys; UPDATE/DELETE return { affected }. Access input via req.data in handlers — don't rely on the return value.

// INSERT — result is iterable with generated keys
const result = await INSERT.into(Products).entries(
  { title: 'Book 1', price: 29.99, currency_code: 'EUR' },
  { title: 'Book 2', price: 19.99, currency_code: 'EUR' }
)
const [book1, book2] = [...result]   // generated IDs

// Single INSERT
const [newProduct] = await INSERT.into(Products).entries({ title: 'New Product', price: 29.99, currency_code: 'EUR' })

// UPDATE — returns { affected: number }
const { affected } = await UPDATE(Products, id).with({ price: 24.99 })

// UPSERT (CAP 7+)
await UPSERT.into(Products).entries(data)

// DELETE — returns { affected: number }
const { affected: gone } = await DELETE.from(Products).where({ ID: id })

Using srv.run() for cross-service calls

// Call another service from within a handler
const db = await cds.connect.to('db')
const AdminSrv = await cds.connect.to('AdminService')

// Run multiple operations in the same transaction
await cds.run([
  UPDATE(Stock).set({ quantity: q.quantity - req.data.amount }).where({ product_ID: id }),
  INSERT.into(AuditLog).entries({ action: 'ORDER', ... })
])

Custom actions and functions

// In .cds:
service OrderService {
  action  submitOrder(orderID: UUID) returns Boolean;   // action = modifies data
  function getStatus(orderID: UUID) returns String;     // function = read-only
}
// In handler:
this.on('submitOrder', async (req) => {
  const { orderID } = req.data
  // ... process
  return true
})

this.on('getStatus', async (req) => {
  const order = await SELECT.one(Orders, req.data.orderID)
  return order?.status ?? 'UNKNOWN'
})

Error handling in handlers

// Reject with HTTP status (stops processing, sends error response)
req.reject(422, 'Quantity must be positive')

// Error with i18n key
req.reject(409, 'ORDER_ALREADY_CLOSED', [req.data.orderID])

// Warn (adds to @SAP.Messages, processing continues)
req.warn(200, 'Stock is low for product {0}', [product.title])

// Programmatic error
throw new cds.error('Something went wrong', { status: 500 })

Accessing the user and locale

const { user, locale, tenant } = req
const isAdmin = user.is('admin')
const userName = user.id  // e.g. 'alice@example.com'

Common mistakes to avoid

  • ❌ Forgetting await super.init() in class-based handlers
  • ❌ Using req.query directly instead of the CQL fluent API
  • ❌ Doing DB reads inside after handlers in a loop (N+1 queries)
  • ❌ Missing return in on handlers (returns undefined → empty response)
  • ❌ Mutating req.data inside after handlers (it's the result, not the input there)
  • ❌ Using cds.db.run() directly when srv.run() or await query pattern suffices