Querying APIs for Composable Data

Create fully custom callable APIs with access to stored and ledger data.

To query data from Zephyr there are two options: using the standardized GrapQl API, or setting up a querying endpoint through a sererless function. The second option is what will be covered in this chapter and has the following advantages.

Advantages of building your API

We already talked about these advantages before, now let's be more specific:

  • Full customization: you can configure your endpoint to accept and return data in your preferred format, and aggregate it within the function as needed.

  • Combine table and ledger queries within the same query/execution: for example, query an address from a table and use it as a parameter to retrieve a ledger entry.

  • This level of flexibility allows for minimizing the amount of data stored in the database, along with costs. You only need to save key data in your tables, which can then be used to retrieve additional required information from the current state of the ledger as needed.

  • Lastly, as previously discussed, working with Zephyr enables you to directly parse table, event, and ledger data within the program using Rust and the Soroban type system. This approach allows you to format the return data as desired, thereby reducing the workload on the caller.

Building your custom API

The setup remains consistent: create a serverless function and handle a request. Within the function, you have the freedom to combine various functions learned so far to create a custom API that will return some specific data. Retrieve ledger/table data, aggregate and modify it, and return the result in your desired format. Here’s a simple example of how you can create an API that composes data previously stored in tables and ledger data:

#[no_mangle]
pub extern "C" fn get_signers_by_address() {
    let env = EnvClient::empty();
    let request: Request = env.read_request_body();
    let borrower: Borrower = env.read_filter().column_equal_to("borrowers", request.borrower).read().unwrap();
    let res: i128 = env.read_contract_entry_by_key([0;32], DataKey::Balance(env.from_scval(&borrower.address))).unwrap().unwrap();

    env.conclude(&res)
}

In the request body, we have a borrower field of type Borrower (which of course, as seen here, needs to be imported into the program). With the read:filter() function, we're accessing our tables, and more specifically the "borrowers" columns, where a list of Borrower objects is stored. We're taking just the borrower that matches our request.

For the sake of simplicity we are omitting some repetitive passages, but make sure to have defined the Request type in your program beforehand (look at General Concepts for reference). Then, by using the borrower.address field of the object we just found, we read from the ledger the Balance entry for that address for a certain contract and return it.

As you can see, there's no limit to what can be done with Zephyr. This was just a simple demonstration of how a more complex data aggregation can be done to create an API with Zephyr, but for a more complex example, you can take a look at Blend's indexing program.


Resources

Last updated