CAP
BTP & Integrationintermediate

Building and Exposing Services with CDS: Data Modeling to Runtime

A practical walkthrough of designing CDS data models, defining services with projections and annotations, and understanding how CAP turns these declarations into a running OData/REST API with authorization enforcement.

Explanation

Once the purpose of CAP is understood, the next step for a consultant is learning how to actually build with Core Data Services (CDS), the declarative language that underlies both the persistence model and the service layer. CDS files use a concise syntax to define entities, types, associations, and compositions, and this same language is reused to define the services that expose those entities to consumers, which keeps the mental model consistent from database to API. In the domain model (typically under db/), entities represent persistent business objects. Associations model relationships to other entities (for example, an Order associated with a Customer), while compositions model ownership relationships where child records are deleted along with the parent (for example, an Order composed of OrderItems). CDS also supports reusable types, aspects (mixins that add common fields like createdAt/modifiedAt), and managed associations that CAP automatically resolves into foreign keys at the database layer. The service layer (typically under srv/) does not usually re-declare entities from scratch; instead, it projects entities from the domain model using CDS projections (SELECT from), which allows a service to expose a filtered, renamed, or restricted view of the underlying data without duplicating the model. This projection approach is central to CAP's philosophy: the same domain model can back multiple services - an internal administrative service exposing full CRUD, and an external partner-facing service exposing only a read-only subset of fields - all from one canonical model. Authorization is expressed declaratively using CDS annotations rather than imperative code. Annotations such as @requires (restricting access to specific roles) and @restrict (fine-grained CRUD-level restrictions per role) are attached to services or entities, and the CAP runtime enforces them automatically against the roles/scopes present in the caller's JWT token (issued by XSUAA in a deployed BTP environment). This is a major shift from hand-coded authorization checks: the security policy lives next to the model it protects, making it easier to review and audit. Once a service definition is compiled, the CAP runtime performs several automatic tasks at request time: it parses the incoming OData or REST request, translates it into a database query (using CQL, CAP's SQL-like query language, under the hood), applies any custom handler logic registered for that event (before/on/after phases), executes the query against the bound database service, and serializes the result back into the wire format the client expects. Developers hook into this flow with custom handlers (srv/*.js or *.java files) attached to specific events - for example, a before CREATE handler to validate input, or an after READ handler to enrich the response with computed fields. For local development, CAP mocks external dependencies: SQLite substitutes for SAP HANA Cloud, and a configurable mock user substitutes for XSUAA-issued tokens, letting a developer test role-based access scenarios without a live subaccount. Before deployment, cds build compiles CDS artifacts into deployable database and service artifacts, and cds deploy (or the HANA deployment tooling) applies the schema to the target database. Consultants should validate not just functional correctness locally but also confirm SQL dialect compatibility with SAP HANA Cloud, since some CDS types and functions behave differently across database backends. Understanding this modeling-to-runtime pipeline is essential before tackling integration scenarios (calling remote OData/REST services from CAP), event-driven patterns (emitting and consuming events via SAP Event Mesh), and deployment topology decisions covered later in this topic.

Code example

ABAP Code
// db/schema.cdsnamespace my.orders; entity Customers { key ID   : UUID; name     : String(120); orders   : Association to many Orders on orders.customer = $self;} entity Orders { key ID     : UUID; customer   : Association to Customers; items      : Composition of many OrderItems on items.order = $self; status     : String(20) default 'NEW';} entity OrderItems { key ID    : UUID; order     : Association to Orders; product   : String(80); quantity  : Integer;} // srv/admin-service.cds - full access for back-office roleusing my.orders as my from '../db/schema'; @requires: 'OrderAdmin'service AdminService { entity Orders as projection on my.Orders; entity Customers as projection on my.Customers;} // srv/partner-service.cds - restricted read-only view for external partnersservice PartnerService { @readonly @restrict: [{ grant: 'READ', to: 'PartnerViewer' }] entity Orders as projection on my.Orders { *, items } excluding { status };} // srv/admin-service.js - custom handler example (Node.js)module.exports = (srv) => { srv.before('CREATE', 'Orders', (req) => { if (!req.data.customer_ID) { req.error(400, 'customer_ID is required to create an order'); } });};

Real project scenario

A logistics company builds a CAP application to manage delivery orders. Internal dispatchers need full read/write access through an AdminService, while external carrier partners should only see order items and delivery addresses, never internal cost fields or the ability to change order status. The team models Orders and OrderItems once in the shared domain model, then defines two separate services - AdminService with @requires('OrderAdmin') and PartnerService with @restrict annotations limiting partners to READ on a filtered projection. A custom before-CREATE handler validates that every new order references a valid customer before it reaches the database, preventing orphaned records that previously caused reconciliation issues in a legacy system.

Common mistakes

โ€ข Duplicating entity definitions inside service files instead of using projections on the shared domain model, causing schema drift between services over time. โ€ข Relying only on UI-level hiding of fields instead of using @restrict/@readonly annotations, leaving sensitive fields accessible via direct API calls even when a UI does not display them. โ€ข Forgetting that compositions cascade deletes, and modeling a relationship as Composition when Association was intended, causing unintended data loss. โ€ข Testing authorization only with the local mock user and never validating actual XSUAA role/scope mapping in a deployed subaccount, leading to production access issues discovered late. โ€ข Writing custom handler logic for functionality (basic validation, default values) that CDS annotations could already express declaratively, increasing code to maintain unnecessarily.

Best practices

โ€ข Model relationships deliberately: use Composition only when child records should not outlive the parent, and Association otherwise. โ€ข Keep authorization declarative wherever possible using @requires and @restrict, reserving custom handler code for logic that cannot be expressed declaratively. โ€ข Expose multiple purpose-specific services (admin, partner, public) as projections over one shared domain model rather than duplicating entities. โ€ข Always validate CDS models and queries against SAP HANA Cloud before go-live, since SQLite (used for local development) does not perfectly replicate all HANA SQL behaviors. โ€ข Use before-phase handlers for validation and on-phase handlers for overriding default persistence logic, keeping event-handler responsibilities clear and testable.

Interview angle

A frequent technical interview question is to explain the difference between Association and Composition in CDS, and why that distinction matters for delete behavior and data ownership. Candidates should also be able to explain how declarative authorization annotations (@requires, @restrict) map to enforcement against XSUAA-issued JWT scopes, and describe the request lifecycle (before/on/after handler phases) that CAP exposes for custom logic - this demonstrates practical, hands-on CAP development experience rather than only conceptual knowledge.