Rust Service¶
Preface¶
This guide will explain how to develop a Rust service to serve data to the Graph. We will cover:
- Setting up the HTTP/GraphQL Server in Serving GraphQL Requests
- Creating GraphQL Objects and providing field resolvers in Defining Objects
- Extending the graph using Federation
- Exporting the GraphQL schema for use in federation in Schema Generation
Dependencies¶
This guide will utilize the following dependencies:
axumas the HTTP serverasync-graphqlas graphql server libraryasync-graphql-axumfor GraphQL request handlingtokioas runtime for writing asynchronous rust program with macros and rt-multi-thread feature enabled
More about async-graphql
async-graphql provides a high-performance server-side implementation of the GraphQL specification.
It is designed to leverage Rust's asynchronous programming capabilities (using Tokio or async-std) to handle GraphQL queries efficiently and concurrently.
[dependencies]
async-graphql = "7.0.6"
async-graphql-axum = "7.0.6"
axum = "0.7.5"
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
Serving GraphQL Requests¶
To handle GraphQL requests, we start by creating a server with a placeholder GraphQL schema. We use the async-graphql-axum crate, which provides integration between async-graphql and axum, to create a graphql_handler.
GraphQL Request Handler
use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
extract::Extension,
routing::post,
Router,
};
struct Query;
#[tokio::main]
async fn main() {
let schema = Schema::build(Query, EmptyMutation, EmptySubscription).finish();
let app = Router::new()
.route("/graphql", post(graphql_handler))
.layer(Extension(schema));
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn graphql_handler(
schema: Extension<Schema<Query, EmptyMutation, EmptySubscription>>,
req: GraphQLRequest,
) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
Warning
This is not a working code. schema throws an error without defining the Objects which we do it in the next section.
GraphQLRequest handles incoming GraphQL requests. It wraps the raw HTTP request
data and converts it into a form that can be processed by async-graphql.
GraphQLResponse wraps the results of executing a GraphQL query. It ensures
that the response is correctly formatted as a valid HTTP response that Axum can
send back to the client.
Defining Objects¶
Info
Refer to Async-graphql Book SimpleObject for a full explanation of how to implement various objects and resolvers.
An Object can be defined by using the Object macro from async-graphql crate. A GraphQL object must have
a resolver defined for each field in its impl.
We can use the SimpleObject macro from async-graphql to map all the fields of a struct to a GraphQL object.
use async_graphql::SimpleObject;
#[derive(SimpleObject)]
struct Person {
id: u32,
first_name: String,
last_name: String,
preferred_name: Option<String>,
}
We can write a resolver for the Person. A resolver function resolvers the values for all the fields a GraphQL object.
Defind Object and a Resolver
use async_graphql::Object;
struct Query;
#[Object]
impl Query {
async fn person(&self) -> Person {
Person {
id: 1,
first_name: "foo".to_string(),
last_name: "bar".to_string(),
preferred_name: None,
}
}
}
We can also add additional fields to the Objects that are not initially defined in the struct using Impl. To add a new field
name to the Person Object we implement a name function.
Add name field to Person Object
impl Person {
async fn name(&self) -> String {
match self.preferred_name {
Some(preferred_name) => preferred_name.clone(),
None => format!("{} {}", self.first_name, self.last_name),
}
}
}
Putting everything together, we should be able to run the follwing code,
Simple GraphQL service to provide a Person information
use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
extract::Extension,
routing::post,
Router,
};
struct Query;
#[Object]
impl Query {
async fn person(&self) -> Person {
Person {
id: 1,
first_name: "foo".to_string(),
last_name: "bar".to_string(),
preferred_name: None,
}
}
}
#[derive(SimpleObject)]
struct Person {
id: u32,
first_name: String,
last_name: String,
preferred_name: Option<String>,
}
#[tokio::main]
async fn main() {
let schema = Schema::build(Query, EmptyMutation, EmptySubscription).finish();
let app = Router::new()
.route("/graphql", post(graphql_handler))
.layer(Extension(schema));
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn graphql_handler(
schema: Extension<Schema<Query, EmptyMutation, EmptySubscription>>,
req: GraphQLRequest,
) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
When we send request to the endpoint curl -X POST -H "Content-Type: application/json" -d '{"query":"{ person {id, firstName, lastName, preferredName} }"}' http://127.0.0.1:8000/graphql we should get the following response
{
"data":{
"person":{
"id":1,
"firstName":"foo",
"lastName":"bar",
"preferredName":null
}
}
}
Related¶
Most of the fields of a GraphQL object directly return the value of the field,
but sometimes the the fields are calculated or resolved by a different resolver.
In such cases, we can use the ComplexObject macro to write a user defined resolver.
This can be useful when we want to resolve related Objects.
We need to use complex macro on Person struct for the ComplexObject macro to take effect.
Related Objects
#[derive(SimpleObject)]
#[graphql(complex)]
struct Person {
id: u32,
first_name: String,
last_name: String,
preferred_name: Option<String>,
}
#[derive(SimpleObject)]
struct Pet{
id: u32,
owner_id: u32,
}
#[ComplexObject]
impl Person{
async fn pet(&self) -> Pet {
Pet {
id: 10,
owner_id: 1
}
}
}
Now the Person object should have a pet field that resolvers a Pet object. When we send request to the endpoint,
curl -X POST -H "Content-Type: application/json" -d '{"query":"{ person {id, pet {id}} }"}' http://127.0.0.1:8000/graphql
we should get the following response
{
"data":{
"person":{
"id":1,
"pet":{"id":10}
}
}
}
Federated¶
Info
Refer to Async-graphql Book Apollo Federation for a full explanation on federation using rust.
To enable federation support we can the schema.enable_federation() method.
Enable federation support
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.enable_federation()
.finish();
We can extends the fields of an Object from another service/subgraph using Entities and @key.
Info
Refer to Introduction to Entities for a full explanation on entities.
Extending fields in Pet Object from another service/subgraph
#[derive(SimpleObject)]
struct Pet{
id: u32,
}
struct Query;
#[Object]
impl Query {
#[graphql(entity)]
async fn reference_resolver(&self, id: u32) -> Pet {
Pet { id }
}
}
Info
The reference_resolver function is not visible for the user consuming the API, it's a way of informing the graphql
router/gateway that this subgraph can resolve the id field for the Pet object using the id as the key.
Now we can extend fields in Pet object using the ComplexObject macro.
We need to use complex macro on Pet struct for the ComplexObject macro to take effect.
Add fields to Pet object
#[derive(SimpleObject)]
#[graphql(complex)]
struct Pet{
id: u32,
}
#[derive(SimpleObject)]
struct Toy{
id: u32,
name: String,
}
struct Query;
#[Object]
impl Query {
#[graphql(entity)]
async fn reference_resolver(&self, id: u32) -> Pet {
Pet { id }
}
}
#[ComplexObject]
impl Pet{
async fn toys(&self) -> Toy {
Toy {id, name}
}
}
Schema Generation¶
We can generate schema using schema.sdl_with_options() method. To enable federation declaratives we should
pass SDLExportOptions::new().federation() as an argument to the method.
Generate Schema with federation declaratives
use async_graphql::SDLExportOptions;
#[tokio::main]
async fn main() {
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.enable_federation()
.finish();
let schema_string = schema.sdl_with_options(SDLExportOptions::new().federation());
println!("{}", schema_string)
}