Blazingly fast, yet simple! — GraphQL client written in Rust 🦀
Beginner friendly 🐣
Quick intro
In this article we’ll take a look at how to create your own GraphQL client in Rust and specifically learn how to query data from a subgraph The Graph. This is meant to be a beginner tutorial, so our queries will be very simple — we’ll fetch a list of the top 5 Order objects (from the Decentraland subgraph) along with their Category fields, and then store those categories in another list. Of course you can play around with that and fetch whatever data you wish. Both Rust and GraphQL are really fast, efficient and of course — fun to work with. This article is heavily inspired by Christian Legnitto’s talk “Rust and GraphQL — A match made in heaven”, which you can check out here if you want more of a deep dive. Enough talk, let’s get right into it!
Prerequisites
You don’t need much in order to follow along — being a beginner Rust programmer myself, I tried to keep things as simple as possible :) You’ll only need the latest stable version of Rust, which in my case is v1.50.0
If you want to check your default Rust compiler version, just run rustup show. It’s also a plus if your IDE/Editor has support for Rust and GraphQL — syntax highlighting, intellisense and all that good stuff.
Structure and dependencies
You can go ahead and create a new Rust project by running cargo init. That will give you a bootstrapped hello world project. Our final project structure will look like this:
Don’t worry about query.graphql and schema.graphql for now, you can just create them and leave them blank. Most of the magic is about to happen in main.rs!
Now, switch over to your Cargo.toml file (analogous to package.json if you’ve used JS) and add the following code under [dependencies]:
[dependencies]
# This is going to perform most of the graphql legwork for us
graphql_client = "0.9.0"
# These three below are for json serialization and deserialization
serde = "1.0.114"
serde_derive = "1.0.114"
serde_json = "1.0.56"
# For making http requests
reqwest = {version = "0.11.0", features = ["json"]}
# This one enables us to use async operations
tokio = {version = "1.1.1", features = ["full"]}
# Error handling crate
anyhow = "1.0.39"
I won’t go in depth about what each of those do, if you’re interested you can check each dependency’s documentation in Rust docs. I’ve added brief comments on what each of them are used for (the lines starting with #, you can remove them if you wish). At this point you can run cargo build to check if everything is OK with the dependencies.
Imports and some ground layer
Time to go back to our main.rs file and import all of those dependencies we’ve just added, just pluck these up on top of the file:
use graphql_client::{GraphQLQuery, Response};
use serde_derive::{Deserialize, Serialize};
use anyhow::anyhow;
use anyhow::Error;
use anyhow::Result as AnyhowResult;
Now, let’s create some objects that’ll help us organise our data better later on:
#[derive(Serialize, Deserialize, Debug, PartialEq)]#[serde(rename_all = "snake_case")]
enum Category {
Estate,
Parcel,
Wearable,
End,
}#[derive(Serialize, Deserialize, Debug)]
struct Order {
category: Category,
}
So far so good — we’ve got an enum for the Category and a struct for the Order, along with some useful annotations that we’ll need.
Constructing the Query struct
This part is a bit more tricky, but we’ll get through it nicely! We need to create a query struct that will use the graphql_client crate (you can read more about how it works here) so we can fill in and integrate our .graphql files in our Rust code. Add the following snippet:
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "src/schema.graphql",
query_path = "src/query.graphql",
response_derives = "Debug, Serialize, Deserialize"
)]
pub struct MyQuery;
What we’re saying in that block is — Construct a query struct called MyQuery, that will derive some traits that we’ll need later on — Debug, Serialize, Deserialize, and that will have a pre-defined query and schema, it’s imperative that we have those files, because Rust needs to know both the schema and the query at compile time.
Schema and Query .graphql files
Remember those empty query.graphql and schema.graphql files we added in the beginning? It’s finally time to use them (I’m assuming at least some familiarity with GraphQL at this point, if you want to learn more please visit their official page). Let’s talk about the schema first. Luckily, the developers that built graphql-rust provide a very useful CLI for this. Please follow the instructions in the readme file there, in order to generate the contents for your schema.graphql file. The schema url for this tutorial is:
https://api.thegraph.com/subgraphs/name/decentraland/marketplace
In the end it should be a pretty large file, so I can’t really give an example of it here.
Now let’s also populate our query.graphql file! For this step you can use graphiql or graphql-playground. Here’s an example query that I’m going to be using:
query MyQuery {
orders(first: 5) {
category
}
}
That small query will send a request to the Decentraland API and fetch the 5 most recent orders, along with their categories. Again, you can tinker with this in any way you want, in order to get more data and make it more fun.
Function to perform the query
Phew! Thankfully we got through that bit, the rest should be easy peasy. Now we need an async function to actually use our query struct — MyQuery and fetch the described data from the API, here’s how it looks like:
async fn perform_my_query(variables: my_query::Variables,
) -> Result<Response<my_query::ResponseData>, Error> { let request_body = MyQuery::build_query(variables);
let client = reqwest::Client::new(); let res = client.post("https://api.thegraph.com/subgraphs/name/decentraland/marketplace")
.json(&request_body)
.send()
.await?; let response_body: Response<my_query::ResponseData> = res.json().await?; Ok(response_body)
}
Calling the query function in main()
Here comes the culmination of all our hard work! We’re going to call perform_my_query() from the main(), feel free to swap the contents of your main() function with the following snippet:
#[tokio::main]
async fn main() -> AnyhowResult<(), Error> { let variables = my_query::Variables; let orders_list = perform_my_query(variables)
.await?
.data
.ok_or(anyhow!("Query failed"))?
.orders; let mut categories: Vec<Category> = Vec::new(); for raw_order in orders_list {
let order_value = serde_json::to_value(raw_order).expect("Failed converting raw order to json value"); let order: Order = serde_json::from_value(order_value)
.expect("Failed converting json value to order object"); categories.push(order.category);
} println!("{:?}", categories);
Ok(())
}
Voilà! We’re now sending off our query request, receiving the desired data and storing the categories in a vector 🎉 , all the while handling errors gracefully and returning helpful error messages to the user (or any code that will call our code in the future). All that’s left now is to run cargo run and see what categories we get back from the API. The output of the program should look something like this, depending on what the categories of the 5 most recent objects are:
[Estate, Estate, Parcel, Wearable, Parcel]
Epilogue
The full code for this tutorial can be found in this repo — https://github.com/arthas168/rust-graphql-client.
Thanks a lot for sticking along and exploring this interesting topic with me! I hope you had as much fun as I did writing this. Stay tuned for more, and feel free to follow me on whichever social media below you prefer 🚀. Also if you have any questions or wish to contribute to this example, you are more than welcome to do so. ’Till next time, and keep building! 🦀 ⚙️
Find me on
GitHub — https://github.com/axiomatic-aardvark
LinkedIn —https://www.linkedin.com/in/petko-pete-pavlovski-%F0%9F%A6%80-07486a156/
Twitter — https://twitter.com/AxiomaticArdvrk
Instagram — https://www.instagram.com/5ko.baratheon/
Gmail — petkopavlovski@gmail.com