Technical Articles
Consuming REST APIs with (Cloud) ABAP
API stands for Application Programming Interface, and comprises a set of standards that allow two applications to talk to each other. REST APIs are a certain pattern of building APIs. They are based on the HTTP protocol, sending and receiving JSON or XML data through URIs (uniform resource identifier). JSON-based REST APIs are prevalent. We will also be using such one in this tutorial.
OData, which is very popular in the SAP world, is itself a REST API. There is a lot of information out there on how to provide a REST API from ABAP (i.e., to publish an OData service). But there isn’t much on how to consume an external API in ABAP. And from what little there is, it includes non-whitelisted ABAP APIs, i.e., they cannot be used with Cloud ABAP. So, I decided to write this tutorial on consuming REST APIs using Cloud ABAP.
Scenario
API Provider
We will be working with JSON Placeholder – a “free to use fake Online REST API for testing and prototyping”. It will allow us to perform all CRUD (create, read, update, delete) actions. To be fair, the create, update, and delete will not actually work, but the server will fake it as if they do. Which is completely enough for our use-case!
Resources
Our API provider exposes posts, comments, albums, photos, TODOs, and users. For simplicity’s sake, we will only be using the posts resource, and pretend the rest aren’t there. The main idea of my tutorial is to provide an extremely simple guide on how to execute the CRUD actions on a REST API. And do this using whitelisted ABAP APIs in the SAP Cloud Platform (CP). This means that you can run this code on a SAP CP trial account.
Posts resource
A post has an ID, title, body and user ID, meaning the ID of the user that created the post. We represent it in ABAP as follows:
TYPES:
BEGIN OF post_s,
user_id TYPE i,
id TYPE i,
title TYPE string,
body TYPE string,
END OF post_s,
post_tt TYPE TABLE OF post_s WITH EMPTY KEY,
BEGIN OF post_without_id_s,
user_id TYPE i,
title TYPE string,
body TYPE string,
END OF post_without_id_s.
We need the structure without ID because the post ID is automatically assigned by the REST API. Meaning that we do not provide it when creating a new post.
Cloud ABAP APIs used
Sending HTTP requests
As I mentioned earlier, the small number of existing tutorials for consuming REST APIs in ABAP primarily use non-whitelisted ABAP APIs. For example, the if_http_client one, whose use is not permitted in Cloud ABAP. The way to check the whitelisted ABAP APIs for the SAP Cloud Platform is to browse the Released Objects lists. It is accessible in Eclipse ABAP Development Tools (ADT) -> Project Explorer -> Released Objects. So, the cloud-ready ABAP API to send HTTP request is the if_web_http_client. We define the following method to get a client:
[definition]
METHODS:
create_client
IMPORTING url TYPE string
RETURNING VALUE(result) TYPE REF TO if_web_http_client
RAISING cx_static_check
[implementation]
METHOD create_client.
DATA(dest) = cl_http_destination_provider=>create_by_url( url ).
result = cl_web_http_client_manager=>create_by_http_destination( dest ).
ENDMETHOD.
Notice that the URL is an input parameter. The returned result is the created web HTTP client.
Working with JSON
To work with JSON, we will be using the Cloud Platform edition of the XCO (Extension Components) library. Read more about it here and here. The specific class, relevant to our use case is xco_cp_json. Something extremely valuable it provides is the ability to transform different naming conventions. For example, camelCase to under_score, and the other way around.
Consuming the REST API
Before getting to the fun part, we just have to define a few constants. Of course, this is not strictly necessary, but working with constants as opposed to string literals is a better practice, and allows for reusability.
CONSTANTS:
base_url TYPE string VALUE 'https://jsonplaceholder.typicode.com/posts',
content_type TYPE string VALUE 'Content-type',
json_content TYPE string VALUE 'application/json; charset=UTF-8'.
The base URL is simply the address of the posts resource. The latter two constants we need for the cases where we will be sending data (i.e., create and update) to the server using the REST API. We have to let the server know we are sending JSON.
Read all posts
The URL for reading all posts is just the base URL. So, we create a client for it, use the client to execute a GET request, close the client, and convert the received JSON to a table of posts. The table of posts type is defined in the Posts resource section above. You can also refer to the full code at the end.
[definition]
read_posts
RETURNING VALUE(result) TYPE post_tt
RAISING cx_static_check
[implementation]
METHOD read_posts.
" Get JSON of all posts
DATA(url) = |{ base_url }|.
DATA(client) = create_client( url ).
DATA(response) = client->execute( if_web_http_client=>get )->get_text( ).
client->close( ).
" Convert JSON to post table
xco_cp_json=>data->from_string( response )->apply(
VALUE #( ( xco_cp_json=>transformation->camel_case_to_underscore ) )
)->write_to( REF #( result ) ).
ENDMETHOD.
Read single post
The method to read a single post is similar, with the differences that we take as an input an ID of the post, and return a structure (i.e., a single post,) instead of a table (i.e., a list of posts). The REST API’s URL of reading a single post is as follows:
https://jsonplaceholder.typicode.com/posts/{ID}
[definition]
read_single_post
IMPORTING id TYPE i
RETURNING VALUE(result) TYPE post_s
RAISING cx_static_check
[implementation]
METHOD read_single_post.
" Get JSON for input post ID
DATA(url) = |{ base_url }/{ id }|.
DATA(client) = create_client( url ).
DATA(response) = client->execute( if_web_http_client=>get )->get_text( ).
client->close( ).
" Convert JSON to post structure
xco_cp_json=>data->from_string( response )->apply(
VALUE #( ( xco_cp_json=>transformation->camel_case_to_underscore ) )
)->write_to( REF #( result ) ).
ENDMETHOD.
Create post
As explained earlier, posts’ IDs are automatically assigned by the REST API. So, to create a post we will be using the post_without_id_s type. This will be our input parameter. We are going to convert from this ABAP structure to JSON, once again using the XCO library. From there, we create a client. Then, we set the body of the HTTP request we are going to send to be the JSON we just created and we let the server know that we will be sending JSON content-type. Lastly, we execute a POST request, and return the server’s response. If all went good, the server’s response would return us our post, along with its newly generated ID (101, because there are currently 100 posts).
[definition]
create_post
IMPORTING post_without_id TYPE post_without_id_s
RETURNING VALUE(result) TYPE string
RAISING cx_static_check
[implementation]
METHOD create_post.
" Convert input post to JSON
DATA(json_post) = xco_cp_json=>data->from_abap( post_without_id )->apply(
VALUE #( ( xco_cp_json=>transformation->underscore_to_camel_case ) ) )->to_string( ).
" Send JSON of post to server and return the response
DATA(url) = |{ base_url }|.
DATA(client) = create_client( url ).
DATA(req) = client->get_http_request( ).
req->set_text( json_post ).
req->set_header_field( i_name = content_type i_value = json_content ).
result = client->execute( if_web_http_client=>post )->get_text( ).
client->close( ).
ENDMETHOD.
Update post
We will be updating with a PUT request. This means we will provide the full post. PATCH, on the other hand, allows us to only provide the updated field (e.g., only title). If you find this interesting, you could try to make the PATCH request yourself – it shouldn’t be too hard with the provided here resources!
We follow a similar logic as with the create action. We also provide a post as an input parameter, but this time we use the full structure (with post ID). The URL for updating a post is the same as accessing this (single) post:
https://jsonplaceholder.typicode.com/posts/{ID}
So, the only differences from create include the changed type of the post input parameter, the URL, and the HTTP request method (PUT).
[definition]
update_post
IMPORTING post TYPE post_s
RETURNING VALUE(result) TYPE string
RAISING cx_static_check
[implementation]
METHOD update_post.
" Convert input post to JSON
DATA(json_post) = xco_cp_json=>data->from_abap( post )->apply(
VALUE #( ( xco_cp_json=>transformation->underscore_to_camel_case ) ) )->to_string( ).
" Send JSON of post to server and return the response
DATA(url) = |{ base_url }/{ post-id }|.
DATA(client) = create_client( url ).
DATA(req) = client->get_http_request( ).
req->set_text( json_post ).
req->set_header_field( i_name = content_type i_value = json_content ).
result = client->execute( if_web_http_client=>put )->get_text( ).
client->close( ).
ENDMETHOD.
Delete post
Deleting a post is the simplest request. We simply take the ID, and send a DELETE HTTP request to the URL of the specific post. To let the user if something goes wrong, we check the server’s response code (should be 200 – meaning OK).
[definition]
delete_post
IMPORTING id TYPE i
RAISING cx_static_check
[implementation]
METHOD delete_post.
DATA(url) = |{ base_url }/{ id }|.
DATA(client) = create_client( url ).
DATA(response) = client->execute( if_web_http_client=>delete ).
IF response->get_status( )-code NE 200.
RAISE EXCEPTION TYPE cx_web_http_client_error.
ENDIF.
ENDMETHOD.
Testing our code
Now that we have provided all the CRUD functionalities, let’s check them out! To do this, we will be implementing the if_oo_adt_classrun interface, which allows to run a class as a console application. It has a main method that gets executed – similar to Java.
METHOD if_oo_adt_classrun~main.
TRY.
" Read
DATA(all_posts) = read_posts( ).
DATA(first_post) = read_single_post( 1 ).
" Create
DATA(create_response) = create_post( VALUE #( user_id = 7
title = 'Hello, World!' body = ':)' ) ).
" Update
first_post-user_id = 777.
DATA(update_response) = update_post( first_post ).
" Delete
delete_post( 9 ).
" Print results
out->write( all_posts ).
out->write( first_post ).
out->write( create_response ).
out->write( update_response ).
CATCH cx_root INTO DATA(exc).
out->write( exc->get_text( ) ).
ENDTRY.
ENDMETHOD.
Running with F9 prints the following output:
Beginning of the output in the ABAP console
[…]
End of the output in the ABAP console
Conclusion
This ends the tutorial of how to consume REST APIs in Cloud ABAP. I hope it has been useful for you. If you feel there’s any points of improvements, or you have any questions or feedback for me, let me know in the comments!
Full code
CLASS zss_tester_2 DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES:
if_oo_adt_classrun.
TYPES:
BEGIN OF post_s,
user_id TYPE i,
id TYPE i,
title TYPE string,
body TYPE string,
END OF post_s,
post_tt TYPE TABLE OF post_s WITH EMPTY KEY,
BEGIN OF post_without_id_s,
user_id TYPE i,
title TYPE string,
body TYPE string,
END OF post_without_id_s.
METHODS:
create_client
IMPORTING url TYPE string
RETURNING VALUE(result) TYPE REF TO if_web_http_client
RAISING cx_static_check,
read_posts
RETURNING VALUE(result) TYPE post_tt
RAISING cx_static_check,
read_single_post
IMPORTING id TYPE i
RETURNING VALUE(result) TYPE post_s
RAISING cx_static_check,
create_post
IMPORTING post_without_id TYPE post_without_id_s
RETURNING VALUE(result) TYPE string
RAISING cx_static_check,
update_post
IMPORTING post TYPE post_s
RETURNING VALUE(result) TYPE string
RAISING cx_static_check,
delete_post
IMPORTING id TYPE i
RAISING cx_static_check.
PRIVATE SECTION.
CONSTANTS:
base_url TYPE string VALUE 'https://jsonplaceholder.typicode.com/posts',
content_type TYPE string VALUE 'Content-type',
json_content TYPE string VALUE 'application/json; charset=UTF-8'.
ENDCLASS.
CLASS zss_tester_2 IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
TRY.
" Read
DATA(all_posts) = read_posts( ).
DATA(first_post) = read_single_post( 1 ).
" Create
DATA(create_response) = create_post( VALUE #( user_id = 7
title = 'Hello, World!' body = ':)' ) ).
" Update
first_post-user_id = 777.
DATA(update_response) = update_post( first_post ).
" Delete
delete_post( 9 ).
" Print results
out->write( all_posts ).
out->write( first_post ).
out->write( create_response ).
out->write( update_response ).
CATCH cx_root INTO DATA(exc).
out->write( exc->get_text( ) ).
ENDTRY.
ENDMETHOD.
METHOD create_client.
DATA(dest) = cl_http_destination_provider=>create_by_url( url ).
result = cl_web_http_client_manager=>create_by_http_destination( dest ).
ENDMETHOD.
METHOD read_posts.
" Get JSON of all posts
DATA(url) = |{ base_url }|.
DATA(client) = create_client( url ).
DATA(response) = client->execute( if_web_http_client=>get )->get_text( ).
client->close( ).
" Convert JSON to post table
xco_cp_json=>data->from_string( response )->apply(
VALUE #( ( xco_cp_json=>transformation->camel_case_to_underscore ) )
)->write_to( REF #( result ) ).
ENDMETHOD.
METHOD read_single_post.
" Get JSON for input post ID
DATA(url) = |{ base_url }/{ id }|.
DATA(client) = create_client( url ).
DATA(response) = client->execute( if_web_http_client=>get )->get_text( ).
client->close( ).
" Convert JSON to post structure
xco_cp_json=>data->from_string( response )->apply(
VALUE #( ( xco_cp_json=>transformation->camel_case_to_underscore ) )
)->write_to( REF #( result ) ).
ENDMETHOD.
METHOD create_post.
" Convert input post to JSON
DATA(json_post) = xco_cp_json=>data->from_abap( post_without_id )->apply(
VALUE #( ( xco_cp_json=>transformation->underscore_to_camel_case ) ) )->to_string( ).
" Send JSON of post to server and return the response
DATA(url) = |{ base_url }|.
DATA(client) = create_client( url ).
DATA(req) = client->get_http_request( ).
req->set_text( json_post ).
req->set_header_field( i_name = content_type i_value = json_content ).
result = client->execute( if_web_http_client=>post )->get_text( ).
client->close( ).
ENDMETHOD.
METHOD update_post.
" Convert input post to JSON
DATA(json_post) = xco_cp_json=>data->from_abap( post )->apply(
VALUE #( ( xco_cp_json=>transformation->underscore_to_camel_case ) ) )->to_string( ).
" Send JSON of post to server and return the response
DATA(url) = |{ base_url }/{ post-id }|.
DATA(client) = create_client( url ).
DATA(req) = client->get_http_request( ).
req->set_text( json_post ).
req->set_header_field( i_name = content_type i_value = json_content ).
result = client->execute( if_web_http_client=>put )->get_text( ).
client->close( ).
ENDMETHOD.
METHOD delete_post.
DATA(url) = |{ base_url }/{ id }|.
DATA(client) = create_client( url ).
DATA(response) = client->execute( if_web_http_client=>delete ).
IF response->get_status( )-code NE 200.
RAISE EXCEPTION TYPE cx_web_http_client_error.
ENDIF.
ENDMETHOD.
ENDCLASS.
Hi Stoyko,
Great work, this is informative blog.
I just have one question if there is any central placeholder for external HTTP connection like transaction SM59 in ABAP in SCP? I found the transaction useful to be able to reuse connection in multiple programs. Additionally if there is one central placeholder, there are activities that can be done easily for example, introducing a proxy server.
The closest thing I can think of is Destinations on SCP but I'm not sure how the destinations can be used in ABAP in SCP
Stev
Hey Stev,
Glad you enjoyed the read!
Unfortunately, I have no idea if there is some SM59-similar functionality in SAP CP. Will let you know if I stumble upon something like this during exploring the platform.
Speaking of exploring SAP CP, the trial got extended to 1 year just last week! Hoping this extended period helps you find something that works for you.
Best,
Stoyko
Thanks for sharing! This will be very helpful. I agree there are not enough posts on this subject. I had to do this just last year and it was challenging to find good examples. I scraped by (because it was a demo anyway) but the resulting code was very clumsy.
These days, people want the code examples on Github, so maybe you'd consider posting there too?
Thanks again.
Hey, Jelena,
Thanks for your feedback! I am very glad the post was helpful to you. Indeed, I was surprised to see that not much information on this topic exists, especially given that the building elements are all there and easy-to-use.
Sharing on GitHub is an excellent solution, I agree. I was actually considering it, but figured it is a relatively small example, for which it would probably be an overkill to open a new repository. But then again, doing this comes with plenty of benefits, most importantly being able to directly pull using abapgit. So I'll be doing it soon and sharing the link!
Best,
Stoyko
And, here it is
PS: this is only the source code, without the metadata that usually comes with abapgit, i.e., it most likely cannot be pulled. I cannot connect to my SAP Cloud Platform account currently, probably because I just extended it for another 30 days, so I’ll make sure to commit it with the abapgit metadata in the coming days.
Edit: commit & push from abapGit also done!
Great! You might want to add the link to the blog post itself too, if haven't done so already.
Thank you!
Great post. As Jelena also noticed and RT. I think XCO is not available for OP yet!
Hey, Muhammad,
Glad it was a useful read for you!
An appropriate substitute for XCO's JSON serialization/deserialization functionality is ui2/cl_json. In fact, I'd say it's about as intuitive as XCO for working with JSONs.
Best,
Stoyko
Hi Muhammad,
starting with ABAP Platform 2020, the XCO Library is also available for On Premise.
Best regards,
Sebastian
Hi P D,
yes, the XCO Library Generation APIs support the programmatic creation and deletion of DDLS (data definition) objects. With CE 2102 the following types of data definitions are supported for both PUT and DELETE operations:
You can refer to https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/02bfcdec55be4365ae8484edbf615879.html for an overview of ABAP Repository object types currently supported by the XCO Library Generation APIs.
To get a first idea of how the content of a DDLS object is specified in the context of the XCO Library Generation APIs you can have a look at method ADD_DDLS in the code sample "RAP BO Generation" (https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/65a7efb4e3114e2aaaf95dc8612c0945.html).
Best regards,
Sebastian
In my RAP Generator The RAP Generator | SAP Blogs I make heavily use of the XCO libraries that have been developed by Sebastians Team.
In a future version I plan to add support for custom entities based on DDIC structures as well.
Hi Stoyko Stoev ,
Thank you for your post!
Could you please share recommended authentication to call API with the code example?
I've tried hardcoding default RFC basic credentials to run in a background as below, but this is not a recommended way I feel so I'm looking for another way with the user logged in credentials dynamically.
I'm guessing OAuth 2.0 authentication will help but not sure how to use it.
My working example:
Jelena Perfiljeva Could you please help in this case if you have any idea?
Thanking you both in advance.
Thank & Regards,
Bhaskar Nagula