Skip to content

Rust Service

Preface

This guide will explain how to develop a Rust service to serve data to the Graph. We will cover:

Dependencies

This guide will utilize the following dependencies:

  • axum as the HTTP server
  • async-graphql as graphql server library
  • async-graphql-axum for GraphQL request handling
  • tokio as 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
            }
        }
}

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)
}