
RAP - API Pattern
In this article, we look at the API pattern for RAP and how you can use it flexibly in ABAP development to provide interfaces.
Table of contents
In this article, we'll look at the API pattern, how you can implement it, and how you can meaningfully integrate it into your development.
Introduction
The ABAP RESTful Application Programming Model is the new model in ABAP for creating cloud-ready and clean core applications. With RAP, you can provide not only applications but also interfaces for internal and external use. With the latest features, RAP is very flexible in terms of structure and use, which is why we would like to divide the applications into different patterns.
Structure
The "API Pattern" is used to create endpoints for processing data in the system. We use the model's standard features, such as structure, functions, actions, and mapping, to prepare the data for further processing. Because we use an unmanaged approach, we have flexibility regarding what happens after the data is processed in the system. We use this key for the structure:
The following characteristics serve as a distinction:
- Definition of the interface structure via a custom entity
- Use of the RAP standard for validation (unmanaged)
- No draft required, therefore OData v2
- Further processing within the system is dynamic
The model has a relatively simple structure; the entity forms the input structure for the interface and uses the behavior for validation and the next steps. This keeps the RAP object very simple and allows for flexible further development.
Example
To do this, we create an API endpoint in the system for different locations (location), which we want to receive via the interface, check, and forward to the API in the database.
Service
In the first step, we need the service description in the form of a custom entity. This defines the structure of the interface for the caller and the information we need. We don't need a query class for reading, since the class only accepts data.
@EndUserText.label: 'Location API'
define root custom entity ZBS_R_APILocation
{
key LocationId : abap.char(15);
LocationName : abap.char(80);
LocationCoordinates : abap.char(35);
LocalPeople : abap.int4;
}
We then generate a service definition to expose the entity externally.
@EndUserText.label: 'Location API'
define service ZBS_API_LOCATION_ENDPOINT {
expose ZBS_R_APILocation as Location;
}
So that we can call the service from outside, we create a service binding of the Web API type. The OData v2 version is sufficient here, as we don't need a draft. After activation and release, the endpoint should be available.
Behavior
With three objects, we have already created the body of the interface, but now we need a behavior to receive and process the data. The flow through the implementation looks like this.
To do this, we define the following behavior definition in the system. The definition is unmanaged because we want to further process the data via a different method. We can generally enable STRICT(2) because we will adhere to the RAP rules in processing. Since we only want to create data records via the interface, CREATE is sufficient. We create the mapping because we later want to map the input data to our database, where we will persist the data for testing.
unmanaged implementation in class zbp_bs_api_location unique;
strict ( 2 );
define behavior for ZBS_R_APILocation alias Location
lock master
authorization master ( instance )
{
create ( authorization : global );
field ( readonly : update ) LocationId;
mapping for zbs_api_target {
LocationId = location_id;
LocationName = location_name;
LocationCoordinates = location_coordinates;
LocalPeople = local_people;
}
}
Finally, we create the empty behavior implementation. The CREATE method, where the data is processed, is important for us. In this method, we transfer the data to the buffer; in principle, we could already perform validations and return messages here.
METHOD create.
INSERT LINES OF entities INTO TABLE lcl_buffer=>new_entries.
ENDMETHOD.
As usual, the buffer class is kept very simple to hold the data during processing.
CLASS lcl_buffer DEFINITION.
PUBLIC SECTION.
CLASS-DATA new_entries TYPE TABLE FOR CREATE zbs_r_apilocationlocation.
ENDCLASS.
The next step is to check the data. To do this, we use the CHECK_BEFORE_SAVE method in the Saver class. At this point, the interaction phase is already complete, and we have all the data in the buffer to validate it. We go through all the data and check the location ID for correctness. The population should also be greater than zero. If there are any errors, we can set FAILED and report the errors to the caller via REPORTED.
METHOD check_before_save.
LOOP AT lcl_buffer=>new_entries INTO DATA(new).
IF NOT is_location_id_valid( new-LocationId ).
INSERT VALUE #( %key = new-%key ) INTO TABLE failed-location.
INSERT VALUE #( %key = new-%key
%msg = new_message( id = 'ZBS_DEMO_RAP_PATTERN'
number = '011'
severity = if_abap_behv_message=>severity-error
v1 = new-LocationId ) )
INTO TABLE reported-location.
ENDIF.
IF new-LocalPeople <= 0.
INSERT VALUE #( %key = new-%key ) INTO TABLE failed-location.
INSERT VALUE #( %key = new-%key
%msg = new_message( id = 'ZBS_DEMO_RAP_PATTERN'
number = '012'
severity = if_abap_behv_message=>severity-error ) )
INTO TABLE reported-location.
ENDIF.
ENDLOOP.
ENDMETHOD.
In the final step, we want to save or further process the data. At this point, you could call a BAPI, a standard RAP object, or other logic. We map the data to the database using the mapping and pass it to the local API, which takes care of the backup. If you're interested in the implementation, you can find the class also in Git.
METHOD save.
DATA(new_database_entries) = CORRESPONDING zcl_bs_api_target=>locations( lcl_buffer=>new_entries MAPPING FROM ENTITY ).
NEW zcl_bs_api_target( )->save_locations( new_database_entries ).
ENDMETHOD.
This completes the implementation of the interface, and we can now test the endpoint.
Test
In this chapter, we will test the endpoint with various scenarios and check its behavior when processing data. We will use Postman for our tests, but we will not explicitly explain how to handle the URL or the x-csrf-token. You can find more information in this article.
Create (Error)
In the first case, let's test the validation of the interface and send incorrect data to the backend. To start the installation, we need a POST and our corresponding data as JSON.
Here you can find the payload we use for the test.
{
"LocationId": "001",
"LocationName": "New York",
"LocationCoordinates": "40° 43′ N, 74° 0′ W",
"LocalPeople": 0
}
In response, we receive a "Bad Request" (status 400). If you go through the XML, you'll also find the two error messages. The residents are Zero and the location ID is also incorrect. This allows validation on the RAP side to work.
<?xml version="1.0" encoding="utf-8"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<code>ZBS_DEMO_RAP_PATTERN/012</code>
<message xml:lang="en">People must be greater than zero.</message>
<innererror>
<application>
<component_id/>
<service_namespace>/SAP/</service_namespace>
<service_id>ZBS_API_LOCATION_ENDP_O2</service_id>
<service_version>0001</service_version>
</application>
<transactionid>B2BD67F2A8D900B0E0068503E872AF7D</transactionid>
<timestamp>20250616193103.6384280</timestamp>
<Error_Resolution>
<SAP_Transaction>For backend administrators: use ADT feed reader "SAP Gateway Error Log" or run transaction /IWFND/ERROR_LOG on SAP Gateway hub system and search for entries with the timestamp above for more details</SAP_Transaction>
<SAP_Note>See SAP Note 1797736 for error analysis (https://service.sap.com/sap/support/notes/1797736)</SAP_Note>
</Error_Resolution>
<errordetails>
<errordetail>
<ContentID/>
<code>ZBS_DEMO_RAP_PATTERN/012</code>
<message>People must be greater than zero.</message>
<longtext_url>/sap/opu/odata/iwbep/message_text/T100_longtexts(MSGID='ZBS_DEMO_RAP_PATTERN',MSGNO='012',MESSAGE_V1='',MESSAGE_V2='',MESSAGE_V3='',MESSAGE_V4='')/$value</longtext_url>
<propertyref/>
<severity>error</severity>
<target/>
<transition>true</transition>
</errordetail>
<errordetail>
<ContentID/>
<code>ZBS_DEMO_RAP_PATTERN/011</code>
<message>LocationId 001 has the wrong format</message>
<longtext_url>/sap/opu/odata/iwbep/message_text/T100_longtexts(MSGID='ZBS_DEMO_RAP_PATTERN',MSGNO='011',MESSAGE_V1='001',MESSAGE_V2='',MESSAGE_V3='',MESSAGE_V4='')/$value</longtext_url>
<propertyref/>
<severity>error</severity>
<target/>
<transition>true</transition>
</errordetail>
</errordetails>
</innererror>
</error>
Hint: Filling in the FAILED information triggers a ROLLBACK internally in RAP, meaning no information is saved. If you want to save an application log to be able to trace the messages, you must use the second database connection here.
Create (Single)
In the second step, let's take a payload that works. The location ID is correctly filled in, and residents have been transferred.
{
"LocationId": "L001",
"LocationName": "New York",
"LocationCoordinates": "40° 43′ N, 74° 0′ W",
"LocalPeople": 8804190
}
If we execute the processing, we receive the status "201 Created", which means the processing is complete. If we now check the ZBS_API_TARGET table in the system, the data record has been created.
Create (Group)
With this endpoint, we can currently only create individual data records; creating many data records is not currently possible. However, we can use a little trick here and resort to a BATCH request. This is a collective request with 1 to n operations in the core. However, this requires a very specific request format and isn't always easy to create. To test a batch request, we've created a small tool on our website, the OData Batch Creator. To do this, we load our service's metadata into the tool; this is needed to identify the entities and structures.
We now add the various actions via Action. In our case, we want to create three new data records, so we need three Create actions.
If you then click "Refresh," the payload will be generated in the window and filled with dummy data. Here, you should fill in the data according to your requirements. You must maintain the structure and blank lines, as the batch request requires an exact structure.
In our case, the batch request looks like this.
--batch_id-685066be96bf8
Content-Type: multipart/mixed; boundary=changeset_id-685066be96bfd
--changeset_id-685066be96bfd
Content-Type: application/http
Content-ID: 1
POST Location HTTP/1.1
Content-Type: application/json
{
"LocationId": "L001",
"LocationName": "New York",
"LocationCoordinates": "40° 43′ N, 74° 0′ W",
"LocalPeople": 8801234
}
--changeset_id-685066be96bfd
Content-Type: application/http
Content-ID: 2
POST Location HTTP/1.1
Content-Type: application/json
{
"LocationId": "L002",
"LocationName": "Paris",
"LocationCoordinates": "48° 51′ N , 2° 21′ O",
"LocalPeople": 2113705
}
--changeset_id-685066be96bfd
Content-Type: application/http
Content-ID: 3
POST Location HTTP/1.1
Content-Type: application/json
{
"LocationId": "L003",
"LocationName": "Pune",
"LocationCoordinates": "18° 31′ N, 73° 51′ O",
"LocalPeople": 3115431
}
--changeset_id-685066be96bfd--
--batch_id-685066be96bf8--
With a batch request, it's important that we set the URL and content type correctly. Instead of the entity, we place $batch after the service to define a batch request. We write the batch ID and specify a multipart document in the content type.
If we now execute the batch, we receive all three data sets at once and don't have to process the data one after the other. This makes especially sense if we have a packet of data that we need to compare with each other.
If our data package was accepted without errors, we should receive a "202 Accepted" and another batch in the payload with the status of each individual request. If we check the database for our changes, two new records have been created and one record has been updated.
Complete example
The complete example of the constructed pattern can be found in the ZBS_DEMO_RAP_PATTERN_API package in the GitHub repository. You can see all changes and objects that have been added to the repository via the following commit.
Summary
The API pattern provides a flexible API endpoint with minimal resources that takes care of consistency and error handling. This means you don't have to reimplement every single function; instead, you can build on the RAP framework. In our example, we primarily focused on the entity as a structure, but there are still options for actions and functions. This allows you to easily map deep entities, for example, if you want an input channel for complex structures, similar to function modules.
Since we are not using a UI for the service, you can save yourself all UI annotations and the metadata extension when building the interface. We'll also omit the Query class, as the interface shouldn't provide any data to the caller.
This makes the pattern quite lean and clear during development, but offers great flexibility in accepting and further processing data within the system.
Conclusion
The API pattern serves as a simple input channel for storing data and further processing within the system. Within the RAP framework, you'll be given some steps and rules for free, and you can use them for a clean implementation.