Skip to Content
Technical Articles
Author's profile photo Stoyko Stoev

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%20of%20the%20output%20in%20the%20ABAP%20console

Beginning of the output in the ABAP console

[…]

End%20of%20the%20output%20in%20the%20ABAP%20console

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.

Assigned Tags

      13 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Stevanic Artana
      Stevanic Artana

      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

      Author's profile photo Stoyko Stoev
      Stoyko Stoev
      Blog Post Author

      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

      Author's profile photo Jelena Perfiljeva
      Jelena Perfiljeva

      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.

      Author's profile photo Stoyko Stoev
      Stoyko Stoev
      Blog Post Author

      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

      Author's profile photo Stoyko Stoev
      Stoyko Stoev
      Blog Post Author

      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!

      Author's profile photo Jelena Perfiljeva
      Jelena Perfiljeva

      Great! You might want to add the link to the blog post itself too, if haven't done so already.

      Thank you!

      Author's profile photo Muhammad Ilyas
      Muhammad Ilyas

      Great post. As Jelena also noticed and RT. I think XCO is not available for OP yet!

      Author's profile photo Stoyko Stoev
      Stoyko Stoev
      Blog Post Author

      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

      Author's profile photo Sebastian Michler
      Sebastian Michler

      Hi Muhammad,

      starting with ABAP Platform 2020, the XCO Library is also available for On Premise.

      Best regards,

      Sebastian

      Author's profile photo P D
      P D
      Can we create CDS views through XCO library ?
      Author's profile photo Sebastian Michler
      Sebastian Michler

      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:

      • View entities
      • Projection views
      • Views
      • Abstract entities
      • Custom entities

      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

      Author's profile photo Andre Fischer
      Andre Fischer

      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.

      Author's profile photo Bhaskar Nagula
      Bhaskar Nagula

      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:

          "create HTTP client by url
          CALL METHOD cl_http_client=>create_by_url
            EXPORTING
              url                = lv_url
            IMPORTING
              client             = DATA(lo_http_client)
            EXCEPTIONS
              argument_not_found = 1
              plugin_not_active  = 2
              internal_error     = 3
              OTHERS             = 4.
      
      *Authenticate to skip the logon popup
            lo_http_client->authenticate(
              EXPORTING
      *         proxy_authentication = ' '            " Proxy Logon (= 'X')
                client               = client         " R/3 system (client number from logon)
                username             = username       " ABAP System, User Logon Name
                password             = password       " Logon ID
                language             = language       " SAP System, Current Language
              ).

      Jelena Perfiljeva Could you please help in this case if you have any idea?

      Thanking you both in advance.

       

      Thank & Regards,

      Bhaskar Nagula