Lesson 3 of 6
Defining services and using generic providers
9 min
Listen to this lesson
A service is the API your app offers. In CAP, services usually expose projections on domain entities: views that select, rename or exclude elements for one use case. The domain model stays stable while each service shows exactly what its consumers need. This lesson defines the fleet app's services and shows how much CAP serves before you write any code.
Service definitions
The fleet desk's service goes in srv/fleet-service.cds:using { com.contoso.fleet as my } from '../db/schema';service FleetService @(path: '/fleet') { entity Inquiries as projection on my.Inquiries; @readonly entity Topics as projection on my.Topics; action closeInquiry (ID : UUID, resolution : String);}
A second service, AdminService, exposes Topics for maintenance by the fleet desk supervisor. @readonly on Topics in FleetService means agents can read the code list but not change it there.
Actions and functions add operations beyond create, read, update and delete. Use an action for something that changes state (closing an inquiry, with a resolution text) and a function for a read-only computation. Actions make intent explicit: rather than letting any client set status = 'Closed' by a generic update, the service offers closeInquiry, whose handler can check that a resolution is given and that the user may close inquiries.
What the generic providers serve
| Request | Served by CAP without code | Example against FleetService |
|---|---|---|
| Read with filter, sort, paging | OData query options | GET /odata/v4/fleet/Inquiries?$filter=status eq 'New'&$top=20 |
| Search | Searches text elements; tune with @cds.search | GET /odata/v4/fleet/Inquiries?$search=CMC-ALT-100 |
| Create with children | Deep insert along compositions; UUID keys filled | POST an inquiry with two notes in one request |
| Expand related data | Navigation along associations and compositions | GET Inquiries?$expand=notes,topic |
| Input validation | @mandatory, @assert.range, @assert.format, @assert.target | Reject priority 5 or a missing salesOrder |
Generic provider features the fleet app relies on. Paths follow the default /odata/v4 prefix plus the service path.
Declarative validation first
CAP's documentation recommends declaring input checks in the model rather than coding them. @mandatory rejects missing or empty input. @assert.range checks numbers, dates or enumerations (priority must be 1 to 3). @assert.format checks a string against a regular expression, and @assert.target on a managed to-one association checks that the referenced record exists, so an inquiry cannot point to a topic code that is not in the list. Each annotation can carry a custom message. Declarative checks are visible in the model, consistent across services, and need no handler code.
- Request arrivesPOST /odata/v4/fleet/Inquiries from the Fiori app
- AuthenticationThe user is identified (mocked users locally, tokens in the cloud)
- Authorization@requires and @restrict annotations are checked
- Declarative validation@mandatory, @assert.range, @assert.format, @assert.target
- Custom handlersbefore, on and after handlers you registered
- Generic providerDatabase insert, keys and managed fields filled, response returned
Projections can do more than pass entities through. A projection can exclude elements a consumer should not see, rename elements to the consumer's vocabulary, or add a where clause so that, for example, a service for fleet customers only ever exposes inquiries that are not marked internal. Because the restriction is part of the service definition, it applies to every request, and a test can prove it once for the whole API instead of screen by screen. Keep in mind that a projection's filter is a modeling choice, not an authorization rule; user-specific restrictions belong in the authorization annotations covered in course sap-btp-204.
What would you do?
A developer has written a before-create handler that checks whether priority is between 1 and 3 and whether salesOrder is filled. Maria reports that the check works when she creates an inquiry, but not when she edits one. What is the better fix?
- Services expose projections on domain entities, one service per use case and audience.
- Default OData V4 paths are /odata/v4/ plus the service path, adjustable with @path.
- Use actions for state changes with business meaning instead of generic field updates.
- Generic providers serve query options, search, deep insert, expand and validation without code.
- Prefer @mandatory and @assert annotations to hand-written validation; they apply on every write.
Check your understanding · not graded
Lesson check
1. What does @readonly on an entity in a service do?
2. Which annotation rejects an inquiry whose topic code does not exist?
3. Why model closing an inquiry as an action instead of a generic status update?
4. What is the default path of a CAP service named FleetService with @path '/fleet' served over OData V4?