10 · Generic Handlers & CRUD

CAP's built-in generic service providers — what is handled for you and when you need custom handlers.

Lesson 10 — Generic Handlers & Automatic CRUD

What you'll learn

What CAP's generic service providers handle automatically, how deep insert and deep read work, what you need to implement yourself, and the exact lifecycle of a CAP request from HTTP to database.


The "magic" in CAP

When you define a service and run cds watch, CAP automatically handles:

All of this runs without a single line of handler code from you.


The request lifecycle

Every incoming request goes through this pipeline:

HTTP Request
     │
     ▼
  BEFORE handlers     ← your pre-processing hooks (validation, enrichment)
     │
     ▼
  ON handlers         ← your business logic (replaces default behavior)
     │                   If no ON handler: CAP's generic handler runs here
     ▼
  AFTER handlers      ← your post-processing hooks (transform, enrich response)
     │
     ▼
HTTP Response

If you define an ON handler, CAP's generic handler does NOT run. Your handler must call next() if you want both to execute.


What generic handlers do

READ

// CAP does all of this for you automatically:

// 1. Parse the OData URL ($filter, $select, $expand, etc.)
// 2. Translate to SQL:
//    SELECT ID, title, price FROM Books
//    WHERE price < 20
//    ORDER BY title ASC
//    LIMIT 10 OFFSET 0
// 3. Execute against SQLite/HANA
// 4. Format as OData JSON response
// 5. Handle $expand (additional JOINs or sub-SELECTs)
// 6. Add @odata.count if $count=true

CREATE (deep insert)

Given this POST body:

{
  "customerName": "Alice",
  "items": [
    { "product": "Book A", "qty": 2 },
    { "product": "Book B", "qty": 1 }
  ]
}

CAP automatically:
1. Validates required fields
2. Generates UUIDs for all entities
3. INSERTs the Order record
4. INSERTs each OrderItem, linking to the parent Order ID
5. Returns the created tree

This works because the items field is defined as a Composition in CDS.


Managed fields — automatic population

If your entity uses the managed aspect:

using { managed } from '@sap/cds/common';

entity Books : managed {
  key ID: UUID;
  title : String;
}

CAP automatically sets on CREATE:
- createdAt = current timestamp
- createdBy = current user (from JWT / mock auth)

And on UPDATE:
- modifiedAt = current timestamp
- modifiedBy = current user

You never set these manually — CAP handles them.


UUID generation — automatic

If you declare key ID : UUID, CAP generates a UUID for new records automatically. Clients do not need to supply the ID on POST.

POST /odata/v4/catalog/Books
Content-Type: application/json

{ "title": "New Book", "price": 25.00 }

Response:

{
  "ID": "a1b2c3d4-...",   ← auto-generated by CAP
  "title": "New Book",
  "price": 25.00,
  "createdAt": "2024-01-15T10:30:00Z",
  "createdBy": "alice"
}

When generic handlers are NOT enough

You need a custom handler when:

Scenario Example
Business logic on write Recalculate inventory when an order is placed
Computed fields at read time Add a virtual discount based on customer tier
External system calls Create a record in S/4HANA when an order is created
Complex validation Check stock before creating an order
Side effects Send an email when a user registers
Custom auth logic Restrict which rows a user can see based on their attributes

Your first custom handler

Create srv/catalog-service.js (same base name as catalog-service.cds):

const cds = require('@sap/cds');

module.exports = class CatalogService extends cds.ApplicationService {

  async init() {

    // BEFORE READ — add a log
    this.before('READ', 'Books', req => {
      console.log(`User ${req.user.id} is reading Books`);
    });

    // AFTER READ — add a virtual field
    this.after('READ', 'Books', books => {
      for (const book of books) {
        book.inStock = book.stock > 0;
      }
    });

    // ON CREATE — validate before save
    this.before('CREATE', 'Books', req => {
      if (req.data.price < 0) {
        req.error(400, 'Price cannot be negative');
      }
    });

    // Call super.init() to register CAP's generic handlers
    await super.init();
  }
}

Critical: always call await super.init() at the end of your init() method. This registers CAP's generic handlers for operations you didn't override.


Handler execution order

BEFORE handlers (yours, in registration order)
        │
        ▼
ON handlers (yours, OR CAP's generic if you didn't register one)
        │
        ▼
AFTER handlers (yours, in registration order)

Multiple before handlers stack — all run in order. Multiple on handlers — only the first one registered runs (unless it calls next()).


Common mistake: forgetting super.init()

// ❌ WRONG — generic CRUD broken, nothing works
module.exports = class MyService extends cds.ApplicationService {
  async init() {
    this.before('READ', 'Books', req => { /* ... */ });
    // forgot super.init() — CAP's generic READ handler never registered!
  }
}

// ✅ CORRECT
module.exports = class MyService extends cds.ApplicationService {
  async init() {
    this.before('READ', 'Books', req => { /* ... */ });
    await super.init();  // always last
  }
}

Checkpoint ✓

Module 1 complete! You now have a solid foundation in CAP. In Module 2, we go deep into CDS data modeling — associations, compositions, aspects, views, annotations, and everything you need to model real-world business data.

Sign in for progress tracking & Q&A →