Handlers in CAP

Handlers are used to add custom logic to service operations (like CRUD or custom actions/functions). They are typically implemented in JavaScript (Node.js) or Java and are registered on service events.

In SAP CAP, different methods are represented as -

  • GET → READ
  • POST → CREATE
  • PATCH → UPDATE
  • DELETE → DELETE

Main Types of Handlers in SAP CAP

1. Before Handlers (before)

  • It gets executed before the actual operation is performed.
  • It is used for - Input validation, Data modification or Authorization checks, etc.
  • It can modify request data or can reject request using req.error().

2. On Handlers (on)

  • It Replace or fully handle the core processing logic.
  • It is used when you want to override default CAP behavior or Implement custom business logic.

3. After Handlers (after)

  • It gets executed after the operation has finished.
  • It is used for - Enriching response data, Logging or Post-processing etc.
  • It cannot reject the request as the request main logic has already been implemented in On Handler, and After Handler works on response data and useful for formatting the response data.

For Example -

Lets implement custom logic that validates the incoming payload before creating a new record. Once the payload is validated, we can apply any modification on the request data.

Once the custom processing is completed, the record will be created in the corresponding entity/database. Finally, the created record will be returned as the response.

srv/warehouse-service.js
import cds from '@sap/cds';

// refering to the entity which we have defined in our data model. We can also refer to the entity by using the relative path like this : cds.entities('Warehouses') 
// but it is always recommended to use the absolute path to avoid any confusion in case of multiple entities with same name in different namespaces.

// const { Warehouses } = cds.entities('warehouse');   //  -> Relative path to refer to the entity
const { Warehouse } = cds.entities('cap.application.db.schema');    //  -> Absolute path to refer to the entity (RECOMMENDED).



const WarehouseService = async (srv) => {

    //  ######################### CREATE - Handler ##########################

    srv.before('CREATE', 'Warehouses', async (request) => {
        try {
            const { name, owner, address } = request.data;

            if (!name || !owner || !address) {
                return request.error({
                    code: 400,
                    message: 'Invalid request, please check your payload'
                });
            }

            //  We wants to modify the owner field and wants to append it with "- NEW" just for fun ;-)
            request.data.owner = `${request.data.owner} - NEW`;
        }
        catch (error) {
            return request.error({
                code: 500,
                message: error.message
            });
        }
    });


    srv.on('CREATE', 'Warehouses', async (request) => {
        try {
            const { name, owner, address } = request.data;

            const newEntryPayload = {
                name: name,
                owner: owner,
                address: address
            }

            const newEntry = await cds.tx(async (tx) => {
                return await tx.run(INSERT.into(Warehouse).entries(newEntryPayload));
            });

            if (!newEntry) {
                return request.error({
                    code: 500,
                    message: 'Something went wrong while creating the warehouse entry'
                });
            }


            return newEntryPayload;
        }
        catch (error) {
            return request.error({
                code: 500,
                message: error.message
            });
        }
    });


    srv.after('CREATE', 'Warehouses', async (data, request) => {
        //  Just for fun, we wants to log the message in console after creating the entry in database.
        console.log(data);

        console.log(request.data);

        return data;
    });



}

export default WarehouseService;

On the above code, we are defining Handlers for Warehouses because it is the entity exposed in the service definition in the CDS file, as shown below -

srv/warehouse-service.cds
using {cap.application.db.schema} from '../db/schema' ;

service WarehouseService @(path : 'warehouse') {

    entity Warehouses as projection on schema.Warehouse;

}

And we can test it locally by defining the POST call on .http file like -

test-api/warehouse.http
### POST call to Create a new Warehouse
POST http://localhost:4004/odata/v4/warehouse/Warehouses
Content-Type: application/json

{
    "ID": "550e8400-e29b-41d4-a716-446655440010",
    "name" : "Elite Storage",
    "owner" : "Elite Transport Co",
    "address" : "77 Business Zone Panchkula"
}

Functions and Actions

1. Functions

  • A function in SAP CAP is a read-only operation defined in CDS that retrieves or computes data without causing any side effects or modifying the database.
  • Function is used to fetch or calculate data without changing anything.

2. Actions

  • An action in SAP CAP is an operation defined in CDS that performs business logic and can modify data or cause side effects in the system.
  • Actions are used to execute operations that change data or trigger business processes.

Additional Handler Variants

1. Event-Specific Handlers

  • It work with - Standard events like: CREATE, READ, UPDATE, DELETE and Custom events like: myAction, myFunction
srv/warehouse-service.js
this.on('myAction', (req) => {
  return { result: 'Success' };
});

2. Wildcard Handlers

  • It is used to attach handler to multiple or all entities/events.
srv/warehouse-service.js
this.before('*', (req) => {
  console.log('Triggered for all events');
});

For Example -

Lets define and implement a function and an action.

The function will return the total number of warehouse records, while the action will update the owner details of a warehouse.

First, define the function and action in the service CDS file.

srv/warehouse-service.cds
using {cap.application.db.schema} from '../db/schema' ;

service WarehouseService @(path : 'warehouse') {

    entity Warehouses as projection on schema.Warehouse;

    function getWarehouseCount() returns Integer ;

    action updateWarehouseOwner(warehouseId : String, newOwner : String) returns String ;
}

Then, define its custom logic on service js file.

srv/warehouse-service.js
import cds from '@sap/cds';

// refering to the entity which we have defined in our data model. We can also refer to the entity by using the relative path like this : cds.entities('Warehouses') 
// but it is always recommended to use the absolute path to avoid any confusion in case of multiple entities with same name in different namespaces.

// const { Warehouses } = cds.entities('warehouse');   //  -> Relative path to refer to the entity
const { Warehouse } = cds.entities('cap.application.db.schema');    //  -> Absolute path to refer to the entity (RECOMMENDED).



const WarehouseService = async (srv) => {

    //  ######################### Function ##########################
    srv.on('getWarehouseCount', async (request) => {
        try {
            const warehouseRecords = await cds.tx(async (tx) => {
                return await tx.run(SELECT.from(Warehouse));
            });

            if (!warehouseRecords) {
                throw new Error('No Records Found');
            }

            return warehouseRecords.length;
        }
        catch (error) {
            return request.error({
                code: 500,
                message: 'Internal Server Error'
            });
        }
    });




    //  ######################### Actions ##########################
    srv.on('updateWarehouseOwner', async (request) => {
        try {

            const {warehouseId , newOwner} = await request.data ;

            if(!warehouseId || !newOwner){
                throw new Error('Invalid Request, please check payload');
            }


            const updateRecord = await cds.tx(async (tx) => {
                return await tx.run(UPDATE(Warehouse).set({owner : newOwner}).where({ID : warehouseId}));
            });

            if(!updateRecord){
                throw new Error('Failed to Update Warehouse Owner');
            }

            return "Owner details updated Successfully"

        }
        catch (error) {
            return request.error({
                code: 500,
                message: 'Internal Server Error'
            });
        }
    });



}

export default WarehouseService;

We can test it locally by defining the GET endpoints for function and POST endpoint for action on .http file like -

function-action/function-action.http
### Get Warehouse Count
GET http://localhost:4004/odata/v4/warehouse/getWarehouseCount


### Update Owner of a Warehouse
POST http://localhost:4004/odata/v4/warehouse/updateWarehouseOwner
Content-Type: application/json

{
    "warehouseId" : "550e8400-e29b-41d4-a716-446655440001",
    "newOwner" : "John Deo"
}

!!! Its Done !!!