09 · OData V4 in CAP

How CAP maps CDS service definitions to OData V4 — metadata, system query options ($filter, $expand, $select, $top), and Batch.

Lesson 9 — Understanding OData V4 in CAP

What you'll learn

How CAP maps your CDS service to OData V4 — the metadata document, all system query options, batch requests, error response format, and how to call your API from JavaScript without a library.


What is OData V4?

OData (Open Data Protocol) is a REST-based standard for querying and manipulating data. Version 4 is the current standard, supported natively by SAP Fiori, Excel, Power BI, and many other tools.

CAP automatically serves your service as an OData V4 API — you don't write any OData code. The CDS definition is all you need.


The metadata document

Every OData service exposes a metadata document at $metadata:

GET /odata/v4/catalog/$metadata

This is an XML document (EDMX format) that describes every entity, field type, key, navigation, action, and function in your service. Client tools (Fiori, Excel) read this to understand your API shape.

<EntityType Name="Books">
  <Key><PropertyRef Name="ID"/></Key>
  <Property Name="ID"    Type="Edm.Guid"   Nullable="false"/>
  <Property Name="title" Type="Edm.String"  MaxLength="100"/>
  <Property Name="price" Type="Edm.Decimal" Precision="10" Scale="2"/>
</EntityType>

CAP generates this automatically from your CDS model. You never write EDMX manually.


System query options

OData provides powerful built-in query capabilities via URL parameters:

$filter — filter results

GET /odata/v4/catalog/Books?$filter=price lt 20
GET /odata/v4/catalog/Books?$filter=price ge 10 and price le 50
GET /odata/v4/catalog/Books?$filter=contains(title, 'Clean')
GET /odata/v4/catalog/Books?$filter=stock gt 0 and author eq 'Adams'

Filter operators:
| Operator | Meaning | Example |
|---------|---------|---------|
| eq | equals | status eq 'active' |
| ne | not equals | status ne 'archived' |
| lt le | less than / or equal | price lt 20 |
| gt ge | greater than / or equal | stock ge 5 |
| and or not | logical | a gt 1 and b lt 10 |
| contains(f, v) | string contains | contains(title,'CAP') |
| startswith(f,v) | string starts with | startswith(name,'S') |
| endswith(f,v) | string ends with | endswith(email,'.com') |

$select — choose fields

GET /odata/v4/catalog/Books?$select=ID,title,price

Response only includes the listed fields. Reduces payload size — critical for mobile apps.

$orderby — sort results

GET /odata/v4/catalog/Books?$orderby=price asc
GET /odata/v4/catalog/Books?$orderby=title desc,price asc

$top and $skip — pagination

GET /odata/v4/catalog/Books?$top=10&$skip=0   # page 1
GET /odata/v4/catalog/Books?$top=10&$skip=10  # page 2
GET /odata/v4/catalog/Books?$top=10&$skip=20  # page 3

$count — total record count

GET /odata/v4/catalog/Books?$count=true&$top=10

Response includes @odata.count: 1234 — the total count before pagination.

$expand — include related entities

# Include author details with each book
GET /odata/v4/catalog/Books?$expand=author

# Nested expand
GET /odata/v4/catalog/Orders?$expand=items($expand=product)

# Expand with filter on the nested set
GET /odata/v4/catalog/Authors?$expand=books($filter=price lt 20)

Combine multiple options

GET /odata/v4/catalog/Books
  ?$filter=stock gt 0
  &$select=ID,title,price
  &$orderby=price asc
  &$top=5
  &$count=true
  &$expand=author($select=name)

OData response format

{
  "@odata.context": "$metadata#Books",
  "@odata.count": 42,
  "value": [
    {
      "ID": "abc-123",
      "title": "Clean Code",
      "price": 35.00,
      "author": {
        "name": "Robert C. Martin"
      }
    }
  ]
}

Always wrapped in { "value": [...] }. Single entity reads (by key) return the object directly without "value".


OData error format

{
  "error": {
    "code": "404",
    "message": "Books with ID 'xyz' not found.",
    "target": "ID",
    "details": []
  }
}

CAP returns this format automatically for validation errors, not-found, and authorization failures.


Batch requests

OData $batch lets you send multiple requests in one HTTP call — critical for performance in Fiori apps:

POST /odata/v4/catalog/$batch
Content-Type: multipart/mixed; boundary=batch_boundary

--batch_boundary
Content-Type: application/http

GET Books?$select=ID,title HTTP/1.1

--batch_boundary
Content-Type: application/http

GET Authors HTTP/1.1

--batch_boundary--

CAP handles batch requests automatically. Fiori apps use $batch internally for every UI interaction.


Calling OData from JavaScript

No OData library needed for simple cases:

// Fetch all books with price filter
const response = await fetch(
  '/odata/v4/catalog/Books?$filter=price lt 20&$select=ID,title,price',
  { headers: { 'Accept': 'application/json' } }
);
const { value: books } = await response.json();

// Create a book
await fetch('/odata/v4/catalog/Books', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'New Book', price: 19.99 })
});

For Fiori/UI5 apps, use sap.ui.model.odata.v4.ODataModel — it handles $batch, CSRF tokens, and metadata automatically.


Checkpoint ✓

In Lesson 10 we learn about CAP's generic handlers — what CAP handles automatically so you don't have to write repetitive CRUD code.

Sign in for progress tracking & Q&A →