GraphQL in Rust
Tags: rust, graphql, apollo
Published: 2020-12-24 10:25:38

---
title: GraphQL in Rust
published: true
description:
tags: rust, graphql, apollo
canonical_url: https://romankudryashov.com/blog/2020/12/graphql-rust/
---

In today’s article, I’ll describe how to create a GraphQL backend using Rust and its ecosystem. The article provides examples of an implementation of the most common tasks that can be encountered while creating GraphQL API. Finally, three microservices will be combined into a single endpoint using Apollo Server and [Apollo Federation](https://www.apollographql.com/docs/federation/). This allows clients to fetch data from any number of sources simultaneously, without needing to know which data comes from which source.

# Introduction

## Overview

In terms of functionality described project is quite similar to one described in my [previous article](https://romankudryashov.com/blog/2020/02/how-to-graphql/), but now it is written using Rust’s stack. The project’s architecture looks like this:

![architecture](https://dev-to-uploads.s3.amazonaws.com/i/40tt2lu9z1jxpc12fy3s.png)

Each component of the architecture answers several questions that may arise when implementing GraphQL API. The domain model includes data about planets in the Solar System and their satellites. The project has a multi-module structure and consists of the following modules:

-   [planets-service](https://github.com/rkudryashov/graphql-rust-demo/tree/master/planets-service) (Rust)

-   [satellites-service](https://github.com/rkudryashov/graphql-rust-demo/tree/master/satellites-service) (Rust)

-   [auth-service](https://github.com/rkudryashov/graphql-rust-demo/tree/master/auth-service) (Rust)

-   [apollo-server](https://github.com/rkudryashov/graphql-rust-demo/tree/master/apollo-server) (JS)

There are two main libraries for creating a GraphQL backend in Rust: [Juniper](https://github.com/graphql-rust/juniper) and [Async-graphql](https://github.com/async-graphql/async-graphql), but only the latter supports Apollo Federation, so I chose it for the project (there is also an open [issue](https://github.com/graphql-rust/juniper/issues/376) for Federation support in Juniper). Both libraries follow the [code-first](https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/) approach.

Also, PostgreSQL is used for persistence layer implementation, [JWT](https://jwt.io/) — for authentication, and Kafka — for asynchronous messaging.

## Stack of technologies

The following table summarizes a stack of main technologies used in the project:

| Type                         | Name           | Site                                                                | GitHub                                                 |
|------------------------------|----------------|---------------------------------------------------------------------|--------------------------------------------------------|
| Language                     | Rust           | [link](https://www.rust-lang.org)                                   | [link](https://github.com/rust-lang/rust)              |
| GraphQL library              | Async-graphql  | [link](https://async-graphql.github.io/async-graphql/en/index.html) | [link](https://github.com/async-graphql/async-graphql) |
| Single GraphQL endpoint      | Apollo Server  | [link](https://www.apollographql.com/docs/apollo-server/)           | [link](https://github.com/apollographql/apollo-server) |
| Web framework                | actix-web      | [link](https://actix.rs)                                            | [link](https://github.com/actix/actix-web)             |
| Database                     | PostgreSQL     | [link](https://www.postgresql.org)                                  | [link](https://github.com/postgres/postgres)           |
| Event streaming platform     | Apache Kafka   | [link](https://kafka.apache.org/)                                   | [link](https://github.com/apache/kafka)                |
| Container orchestration tool | Docker Compose | [link](https://docs.docker.com/compose/)                            | [link](https://github.com/docker/compose)              |

Also, some other used Rust libraries:

| Type                     | Name              | Site                                            | GitHub                                                      |
|--------------------------|-------------------|-------------------------------------------------|-------------------------------------------------------------|
| ORM                      | Diesel            | [link](https://diesel.rs/)                      | [link](https://github.com/diesel-rs/diesel)                 |
| Kafka client             | rust-rdkafka      | [link](https://crates.io/crates/rdkafka)        | [link](https://github.com/fede1024/rust-rdkafka)            |
| Password hashing library | argonautica       | [link](https://crates.io/crates/argonautica)    | [link](https://github.com/bcmyers/argonautica)              |
| JWT library              | jsonwebtoken      | [link](https://crates.io/crates/jsonwebtoken)   | [link](https://github.com/Keats/jsonwebtoken)               |
| Testing library          | Testcontainers-rs | [link](https://crates.io/crates/testcontainers) | [link](https://github.com/testcontainers/testcontainers-rs) |

## Prerequisites

To launch the project locally you only need Docker Compose. Without Docker, you might need to install the following:

-   [Rust](https://www.rust-lang.org/tools/install)

-   [Diesel CLI](https://diesel.rs/guides/getting-started/) (to install run `cargo install diesel_cli --no-default-features --features postgres`)

-   [LLVM](https://releases.llvm.org/download.html) (this is needed for the `argonautica` crate to work)

-   [CMake](https://cmake.org/install/) (this is needed for the `rust-rdkafka` crate to work)

-   [PostgreSQL](https://www.postgresql.org/download/)

-   [Apache Kafka](https://kafka.apache.org/quickstart)

-   [npm](https://www.npmjs.com/get-npm)

# Implementation

Root `Cargo.toml` specifies three applications and one library:

<div class="formalpara-title" markdown="1">

*Root [Cargo.toml](https://github.com/rkudryashov/graphql-rust-demo/blob/master/Cargo.toml)*

</div>

``` rust
[workspace]
members = [
    "auth-service",
    "planets-service",
    "satellites-service",
    "common-utils",
]
```

Let’s start with [`planets-service`](https://github.com/rkudryashov/graphql-rust-demo/tree/master/planets-service).

## Dependencies

`Cargo.toml` looks like this:

<div class="formalpara-title" markdown="1">

*[Cargo.toml](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/Cargo.toml)*

</div>

``` toml
[package]
name = "planets-service"
version = "0.1.0"
edition = "2018"

[dependencies]
common-utils = { path = "../common-utils" }
async-graphql = "2.4.3"
async-graphql-actix-web = "2.4.3"
actix-web = "3.3.2"
actix-rt = "1.1.1"
actix-web-actors = "3.0.0"
futures = "0.3.8"
async-trait = "0.1.42"
bigdecimal = { version = "0.1.2", features = ["serde"] }
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.60"
diesel = { version = "1.4.5", features = ["postgres", "r2d2", "numeric"] }
diesel_migrations = "1.4.0"
dotenv = "0.15.0"
strum = "0.20.0"
strum_macros = "0.20.1"
rdkafka = { version = "0.24.0", features = ["cmake-build"] }
async-stream = "0.3.0"
lazy_static = "1.4.0"

[dev-dependencies]
jsonpath_lib = "0.2.6"
testcontainers = "0.9.1"
```

`async-graphql` is a GraphQL library, `actix-web` is a web framework, and `async-graphql-actix-web` provides integration between them.

## Core functions

We’ll go right away to the `main.rs`:

<div class="formalpara-title" markdown="1">

*[main.rs](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/main.rs)*

</div>

``` rust
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();
    let pool = create_connection_pool();
    run_migrations(&pool);

    let schema = create_schema_with_context(pool);

    HttpServer::new(move || App::new()
        .configure(configure_service)
        .data(schema.clone())
    )
        .bind("0.0.0.0:8001")?
        .run()
        .await
}
```

Here an environment and HTTP server are configured using functions defined in the `lib.rs`:

<div class="formalpara-title" markdown="1">

*[lib.rs](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/lib.rs)*

</div>

``` rust
pub fn configure_service(cfg: &mut web::ServiceConfig) {
    cfg
        .service(web::resource("/")
            .route(web::post().to(index))
            .route(web::get().guard(guard::Header("upgrade", "websocket")).to(index_ws))
            .route(web::get().to(index_playground))
        );
}

async fn index(schema: web::Data<AppSchema>, http_req: HttpRequest, req: Request) -> Response {
    let mut query = req.into_inner();

    let maybe_role = common_utils::get_role(http_req);
    if let Some(role) = maybe_role {
        query = query.data(role);
    }

    schema.execute(query).await.into()
}

async fn index_ws(schema: web::Data<AppSchema>, req: HttpRequest, payload: web::Payload) -> Result<HttpResponse> {
    WSSubscription::start(Schema::clone(&*schema), &req, payload)
}

async fn index_playground() -> HttpResponse {
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(playground_source(GraphQLPlaygroundConfig::new("/").subscription_endpoint("/")))
}

pub fn create_schema_with_context(pool: PgPool) -> Schema<Query, Mutation, Subscription> {
    let arc_pool = Arc::new(pool);
    let cloned_pool = Arc::clone(&arc_pool);
    let details_batch_loader = Loader::new(DetailsBatchLoader {
        pool: cloned_pool
    }).with_max_batch_size(10);

    let kafka_consumer_counter = Mutex::new(0);

    Schema::build(Query, Mutation, Subscription)
        .data(arc_pool)
        .data(details_batch_loader)
        .data(kafka::create_producer())
        .data(kafka_consumer_counter)
        .finish()
}
```

These functions do the following:

-   `index` — processes GraphQL [queries and mutations](https://graphql.org/learn/queries/)

-   `index_ws` — processes GraphQL [subscriptions](https://www.apollographql.com/docs/react/data/subscriptions/)

-   `index_playground` — provides Playground GraphQL IDE

-   `create_schema_with_context` — creates GraphQL schema with global contextual data that are accessible at runtime, for example, a pool of database connections

## Query and type definition

Let’s consider how to define a query:

<div class="formalpara-title" markdown="1">

*[Query definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
#[Object]
impl Query {
    async fn get_planets(&self, ctx: &Context<'_>) -> Vec<Planet> {
        repository::get_all(&get_conn_from_ctx(ctx)).expect("Can't get planets")
            .iter()
            .map(|p| { Planet::from(p) })
            .collect()
    }

    async fn get_planet(&self, ctx: &Context<'_>, id: ID) -> Option<Planet> {
        find_planet_by_id_internal(ctx, id)
    }

    #[graphql(entity)]
    async fn find_planet_by_id(&self, ctx: &Context<'_>, id: ID) -> Option<Planet> {
        find_planet_by_id_internal(ctx, id)
    }
}

fn find_planet_by_id_internal(ctx: &Context<'_>, id: ID) -> Option<Planet> {
    let id = id.to_string().parse::<i32>().expect("Can't get id from String");
    repository::get(id, &get_conn_from_ctx(ctx)).ok()
        .map(|p| { Planet::from(&p) })
}
```

The queries are getting data from a database using a repository. Obtained entities are converted to GraphQL DTO (that allows us to preserve the single-responsibility principle for each struct). `get_planets` and `get_planet` queries can be accessed from any GraphQL IDE, for example like this:

<div class="formalpara-title" markdown="1">

*Query example*

</div>

``` graphql
{
  getPlanets {
    name
    type
  }
}
```

`Planet` struct is defined as follows:

<div class="formalpara-title" markdown="1">

*[GraphQL type definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
#[derive(Serialize, Deserialize)]
struct Planet {
    id: ID,
    name: String,
    planet_type: PlanetType,
}

#[Object]
impl Planet {
    async fn id(&self) -> &ID {
        &self.id
    }

    async fn name(&self) -> &String {
        &self.name
    }

    /// From an astronomical point of view
    #[graphql(name = "type")]
    async fn planet_type(&self) -> &PlanetType {
        &self.planet_type
    }

    #[graphql(deprecation = "Now it is not in doubt. Do not use this field")]
    async fn is_rotating_around_sun(&self) -> bool {
        true
    }

    async fn details(&self, ctx: &Context<'_>) -> Details {
        let loader = ctx.data::<Loader<i32, Details, DetailsBatchLoader>>().expect("Can't get loader");
        let planet_id = self.id.to_string().parse::<i32>().expect("Can't convert id");
        loader.load(planet_id).await
    }
}
```

Here we define a resolver for each field. Also, for some fields, description (as Rust’s comment) and deprecation reason are specified. These will be shown in a GraphQL IDE.

## N+1 problem

If the definition of `details` function of `Planet` above was *naive*, this would lead to N+1 problem, that is, if you made such a request:

<div class="formalpara-title" markdown="1">

*Example of possible resource-consuming GraphQL request*

</div>

``` graphql
{
  getPlanets {
    name
    details {
      meanRadius
    }
  }
}
```

then separate SQL call would be executed for `details` field of each `planet` because `Details` is a separate entity from `Planet` and is stored in its own table.

But with help of Async-graphql’s implementation of [DataLoader](https://github.com/graphql/dataloader) `details` resolver can be defined as follows:

<div class="formalpara-title" markdown="1">

*[Resolver definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
async fn details(&self, ctx: &Context<'_>) -> Result<Details> {
    let data_loader = ctx.data::<DataLoader<DetailsLoader>>().expect("Can't get data loader");
    let planet_id = self.id.to_string().parse::<i32>().expect("Can't convert id");
    let details = data_loader.load_one(planet_id).await?;
    details.ok_or_else(|| "Not found".into())
}
```

`data_loader` is an application-scoped object defined in this way:

<div class="formalpara-title" markdown="1">

*[DataLoader definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/lib.rs)*

</div>

``` rust
let details_data_loader = DataLoader::new(DetailsLoader {
    pool: cloned_pool
}).max_batch_size(10);
```

`DetailsLoader` is implemented like this:

<div class="formalpara-title" markdown="1">

*[DetailsLoader definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
pub struct DetailsLoader {
    pub pool: Arc<PgPool>
}

#[async_trait::async_trait]
impl Loader<i32> for DetailsLoader {
    type Value = Details;
    type Error = Error;

    async fn load(&self, keys: &[i32]) -> Result<HashMap<i32, Self::Value>, Self::Error> {
        let conn = self.pool.get().expect("Can't get DB connection");
        let details = repository::get_details(keys, &conn).expect("Can't get planets' details");

        Ok(details.iter()
            .map(|details_entity| (details_entity.planet_id, Details::from(details_entity)))
            .collect::<HashMap<_, _>>())
    }
}
```

This method helps us to prevent N+1 problem because each `DetailsLoader.load` call executes just one SQL call returning a bunch of `DetailsEntity`.

## Interface definition

GraphQL interface and its implementations can be defined in this way:

<div class="formalpara-title" markdown="1">

*[GraphQL interface definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
#[derive(Interface, Clone)]
#[graphql(
    field(name = "mean_radius", type = "&CustomBigDecimal"),
    field(name = "mass", type = "&CustomBigInt"),
)]
pub enum Details {
    InhabitedPlanetDetails(InhabitedPlanetDetails),
    UninhabitedPlanetDetails(UninhabitedPlanetDetails),
}

#[derive(SimpleObject, Clone)]
pub struct InhabitedPlanetDetails {
    mean_radius: CustomBigDecimal,
    mass: CustomBigInt,
    /// In billions
    population: CustomBigDecimal,
}

#[derive(SimpleObject, Clone)]
pub struct UninhabitedPlanetDetails {
    mean_radius: CustomBigDecimal,
    mass: CustomBigInt,
}
```

Here you can also see that if the structure doesn’t have a single field with a complex resolver, it can be implemented using `SimpleObject` attribute.

## Custom scalar definition

Custom scalars allow defining how to represent a value of some type and how to parse it. The project contains two examples of custom scalar definition; both are wrappers for numeric structures (because you can’t implement an external trait on an external type due to the [*orphan rule*](https://doc.rust-lang.org/book/ch10-02-traits.html#implementing-a-trait-on-a-type)). The wrappers are implemented as follows:

<div class="formalpara-title" markdown="1">

*[Custom scalar: wrapper for BigInt](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
#[derive(Clone)]
pub struct CustomBigInt(BigDecimal);

#[Scalar(name = "BigInt")]
impl ScalarType for CustomBigInt {
    fn parse(value: Value) -> InputValueResult<Self> {
        match value {
            Value::String(s) => {
                let parsed_value = BigDecimal::from_str(&s)?;
                Ok(CustomBigInt(parsed_value))
            }
            _ => Err(InputValueError::expected_type(value)),
        }
    }

    fn to_value(&self) -> Value {
        Value::String(format!("{:e}", &self))
    }
}

impl LowerExp for CustomBigInt {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let val = &self.0.to_f64().expect("Can't convert BigDecimal");
        LowerExp::fmt(val, f)
    }
}
```

<div class="formalpara-title" markdown="1">

*[Custom scalar: wrapper for BigDecimal](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
#[derive(Clone)]
pub struct CustomBigDecimal(BigDecimal);

#[Scalar(name = "BigDecimal")]
impl ScalarType for CustomBigDecimal {
    fn parse(value: Value) -> InputValueResult<Self> {
        match value {
            Value::String(s) => {
                let parsed_value = BigDecimal::from_str(&s)?;
                Ok(CustomBigDecimal(parsed_value))
            }
            _ => Err(InputValueError::expected_type(value)),
        }
    }

    fn to_value(&self) -> Value {
        Value::String(self.0.to_string())
    }
}
```

The former example also shows how to represent a large number using exponential (scientific) notation.

## Mutation definition

A mutation can be defined as the following:

<div class="formalpara-title" markdown="1">

*[Mutation definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
pub struct Mutation;

#[Object]
impl Mutation {
    #[graphql(guard(RoleGuard(role = "Role::Admin")))]
    async fn create_planet(&self, ctx: &Context<'_>, planet: PlanetInput) -> Result<Planet, Error> {
        let new_planet = NewPlanetEntity {
            name: planet.name,
            planet_type: planet.planet_type.to_string(),
        };

        let details = planet.details;
        let new_planet_details = NewDetailsEntity {
            mean_radius: details.mean_radius.0,
            mass: BigDecimal::from_str(&details.mass.0.to_string()).expect("Can't get BigDecimal from string"),
            population: details.population.map(|wrapper| { wrapper.0 }),
            planet_id: 0,
        };

        let created_planet_entity = repository::create(new_planet, new_planet_details, &get_conn_from_ctx(ctx))?;

        let producer = ctx.data::<FutureProducer>().expect("Can't get Kafka producer");
        let message = serde_json::to_string(&Planet::from(&created_planet_entity)).expect("Can't serialize a planet");
        kafka::send_message(producer, message).await;

        Ok(Planet::from(&created_planet_entity))
    }
}
```

To use an object as input parameter of a mutation you need to define a structure in this way:

<div class="formalpara-title" markdown="1">

*[Input type definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
#[derive(InputObject)]
struct PlanetInput {
    name: String,
    #[graphql(name = "type")]
    planet_type: PlanetType,
    details: DetailsInput,
}
```

The mutation is protected by `RoleGuard` which ensures that only users with `Admin` role can access it. So to execute the mutation, for example, like this:

<div class="formalpara-title" markdown="1">

*Example of mutation usage*

</div>

``` graphql
mutation {
  createPlanet(
    planet: {
      name: "test_planet"
      type: TERRESTRIAL_PLANET
      details: { meanRadius: "10.5", mass: "8.8e24", population: "0.5" }
    }
  ) {
    id
  }
}
```

you need to specify `Authorization` header with a JWT obtained from `auth-service` (this will be described later).

## Subscription definition

In the mutation definition above you could see that on a planet creation a message is sent:

<div class="formalpara-title" markdown="1">

*[Sending a message to Kafka](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
let producer = ctx.data::<FutureProducer>().expect("Can't get Kafka producer");
let message = serde_json::to_string(&Planet::from(&created_planet_entity)).expect("Can't serialize a planet");
kafka::send_message(producer, message).await;
```

The API client can be notified of the event by using a subscription that listens to Kafka consumer:

<div class="formalpara-title" markdown="1">

*[Subscription definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
pub struct Subscription;

#[Subscription]
impl Subscription {
    async fn latest_planet<'ctx>(&self, ctx: &'ctx Context<'_>) -> impl Stream<Item=Planet> + 'ctx {
        let kafka_consumer_counter = ctx.data::<Mutex<i32>>().expect("Can't get Kafka consumer counter");
        let consumer_group_id = kafka::get_kafka_consumer_group_id(kafka_consumer_counter);
        let consumer = kafka::create_consumer(consumer_group_id);

        async_stream::stream! {
            let mut stream = consumer.start();

            while let Some(value) = stream.next().await {
                yield match value {
                    Ok(message) => {
                        let payload = message.payload().expect("Kafka message should contain payload");
                        let message = String::from_utf8_lossy(payload).to_string();
                        serde_json::from_str(&message).expect("Can't deserialize a planet")
                    }
                    Err(e) => panic!("Error while Kafka message processing: {}", e)
                };
            }
        }
    }
}
```

A subscription can be used like queries and mutations:

<div class="formalpara-title" markdown="1">

*Example of subscription usage*

</div>

``` graphql
subscription {
  latestPlanet {
    id
    name
    type
    details {
      meanRadius
    }
  }
}
```

Subscription URL is `ws://localhost:8001`.

## Integration tests

Tests for queries and mutations can be written like this:

<div class="formalpara-title" markdown="1">

*[Query test](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/tests/query_tests.rs)*

</div>

``` rust
#[actix_rt::test]
async fn test_get_planets() {
    let docker = Cli::default();
    let (_pg_container, pool) = common::setup(&docker);

    let mut service = test::init_service(App::new()
        .configure(configure_service)
        .data(create_schema_with_context(pool))
    ).await;

    let query = "
        {
            getPlanets {
                id
                name
                type
                details {
                    meanRadius
                    mass
                    ... on InhabitedPlanetDetails {
                        population
                    }
                }
            }
        }
        ".to_string();

    let request_body = GraphQLCustomRequest {
        query,
        variables: Map::new(),
    };

    let request = test::TestRequest::post().uri("/").set_json(&request_body).to_request();

    let response: GraphQLCustomResponse = test::read_response_json(&mut service, request).await;

    fn get_planet_as_json(all_planets: &serde_json::Value, index: i32) -> &serde_json::Value {
        jsonpath::select(all_planets, &format!("$.getPlanets[{}]", index)).expect("Can't get planet by JSON path")[0]
    }

    let mercury_json = get_planet_as_json(&response.data, 0);
    common::check_planet(mercury_json, 1, "Mercury", "TERRESTRIAL_PLANET", "2439.7");

    let earth_json = get_planet_as_json(&response.data, 2);
    common::check_planet(earth_json, 3, "Earth", "TERRESTRIAL_PLANET", "6371.0");

    let neptune_json = get_planet_as_json(&response.data, 7);
    common::check_planet(neptune_json, 8, "Neptune", "ICE_GIANT", "24622.0");
}
```

If a part of a query can be reused in another query, you can use [fragments](https://graphql.org/learn/queries/#fragments):

<div class="formalpara-title" markdown="1">

*[Query test using fragment](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/tests/query_tests.rs)*

</div>

``` rust
const PLANET_FRAGMENT: &str = "
    fragment planetFragment on Planet {
        id
        name
        type
        details {
            meanRadius
            mass
            ... on InhabitedPlanetDetails {
                population
            }
        }
    }
";

#[actix_rt::test]
async fn test_get_planet_by_id() {
    ...

    let query = "
        {
            getPlanet(id: 3) {
                ... planetFragment
            }
        }
        ".to_string() + PLANET_FRAGMENT;

    let request_body = GraphQLCustomRequest {
        query,
        variables: Map::new(),
    };

    ...
}
```

To use [variables](https://graphql.org/learn/queries/#variables), you can write test in this way:

<div class="formalpara-title" markdown="1">

*[Query test using a fragment and variables](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/tests/query_tests.rs)*

</div>

``` rust
#[actix_rt::test]
async fn test_get_planet_by_id_with_variable() {
    ...

    let query = "
        query testPlanetById($planetId: String!) {
            getPlanet(id: $planetId) {
                ... planetFragment
            }
        }".to_string() + PLANET_FRAGMENT;

    let jupiter_id = 5;
    let mut variables = Map::new();
    variables.insert("planetId".to_string(), jupiter_id.into());

    let request_body = GraphQLCustomRequest {
        query,
        variables,
    };

    ...
}
```

In this project, `Testcontainers-rs` library is used to prepare test environment, that is, to create a temporary PostgreSQL database.

## GraphQL API client

You can use code snippets from the previous section to create a client to an external GraphQL API. Also, there are special libraries for this purpose, for example, [graphql-client](https://github.com/graphql-rust/graphql-client), but I haven’t used them yet.

## API security

There are different security threats to your GraphQL API (see [this checklist](https://leapgraph.com/graphql-api-security) to learn more); let’s consider some of these aspects.

### Query depth and complexity limit

If the `Satellite` structure contained `planet` field, the following request would be possible:

<div class="formalpara-title" markdown="1">

*Example of expensive query*

</div>

``` graphql
{
  getPlanet(id: "1") {
    satellites {
      planet {
        satellites {
          planet {
            satellites {
              ... # more deep nesting!
            }
          }
        }
      }
    }
  }
}
```

To make such a request invalid we can specify:

<div class="formalpara-title" markdown="1">

*[Example of query depth and complexity limit](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/lib.rs)*

</div>

``` rust
pub fn create_schema_with_context(pool: PgPool) -> Schema<Query, Mutation, Subscription> {
    ...

    Schema::build(Query, Mutation, Subscription)
        .limit_depth(3)
        .limit_complexity(15)

    ...
}
```

Note that if you specify depth or complexity limit, then documentation of a service may stop showing in your GraphQL IDE. This is because the IDE tries to perform an introspection query that has considerable depth and complexity.

### Authentication

This functionality is implemented in [`auth-service`](https://github.com/rkudryashov/graphql-rust-demo/tree/master/auth-service) using `argonautica` and `jsonwebtoken` crates. The former crate is responsible for a user’s password hashing using [Argon2](https://en.wikipedia.org/wiki/Argon2) algorithm. Authentication and authorization functionality is only for demo; please do more research for production use.

Let’s consider how signing in is implemented:

<div class="formalpara-title" markdown="1">

*[Signing in implementation](https://github.com/rkudryashov/graphql-rust-demo/blob/master/auth-service/src/graphql.rs)*

</div>

``` rust
pub struct Mutation;

#[Object]
impl Mutation {

    async fn sign_in(&self, ctx: &Context<'_>, input: SignInInput) -> Result<String, Error> {
        let maybe_user = repository::get_user(&input.username, &get_conn_from_ctx(ctx)).ok();

        if let Some(user) = maybe_user {
            if let Ok(matching) = verify_password(&user.hash, &input.password) {
                if matching {
                    let role = AuthRole::from_str(user.role.as_str()).expect("Can't convert &str to AuthRole");
                    return Ok(common_utils::create_token(user.username, role));
                }
            }
        }

        Err(Error::new("Can't authenticate a user"))
    }
}

#[derive(InputObject)]
struct SignInInput {
    username: String,
    password: String,
}
```

You can check the implementation of `verify_password` function in `utils` [module](https://github.com/rkudryashov/graphql-rust-demo/blob/master/auth-service/src/utils.rs) and `create_token` in `common_utils` [module](https://github.com/rkudryashov/graphql-rust-demo/blob/master/common-utils/src/lib.rs). As you may expect, `sign_in` function issues JWT that further can be used for authorization in other services.

To get a JWT you need to perform the following mutation:

<div class="formalpara-title" markdown="1">

*Getting JWT*

</div>

``` graphql
mutation {
  signIn(input: { username: "john_doe", password: "password" })
}
```

For that, you can use *john_doe/password* credentials. Including obtained JWT to further requests allows access to protected resources (see next section).

### Authorization

To request protected data you need to add a header to an HTTP request in the format `Authorization: Bearer $JWT`. `index` function will extract a user’s role from the request and will add it to a query data:

<div class="formalpara-title" markdown="1">

*[Role extraction](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/lib.rs)*

</div>

``` rust
async fn index(schema: web::Data<AppSchema>, http_req: HttpRequest, req: Request) -> Response {
    let mut query = req.into_inner();

    let maybe_role = common_utils::get_role(http_req);
    if let Some(role) = maybe_role {
        query = query.data(role);
    }

    schema.execute(query).await.into()
}
```

The following attribute was applied to `create_planet` mutation defined earlier:

<div class="formalpara-title" markdown="1">

*[Guard usage](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
#[graphql(guard(RoleGuard(role = "Role::Admin")))]
```

Guard itself is implemented as follows:

<div class="formalpara-title" markdown="1">

*[Guard implementation](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
struct RoleGuard {
    role: Role,
}

#[async_trait::async_trait]
impl Guard for RoleGuard {
    async fn check(&self, ctx: &Context<'_>) -> Result<()> {
        if ctx.data_opt::<Role>() == Some(&self.role) {
            Ok(())
        } else {
            Err("Forbidden".into())
        }
    }
}
```

So if you don’t specify a token, the server will respond with a message containing "Forbidden".

## Enum definition

GraphQL enum can be defined in this way:

<div class="formalpara-title" markdown="1">

*[Enum definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/satellites-service/src/graphql.rs)*

</div>

``` rust
#[derive(SimpleObject)]
struct Satellite {
    ...
    life_exists: LifeExists,
}

#[derive(Copy, Clone, Eq, PartialEq, Debug, Enum, EnumString)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum LifeExists {
    Yes,
    OpenQuestion,
    NoData,
}
```

## Work with dates

Async-graphql supports date/time types from `chrono` [library](https://crates.io/crates/chrono), so you can define such fields as usual:

<div class="formalpara-title" markdown="1">

*[Date field definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/satellites-service/src/graphql.rs)*

</div>

``` rust
#[derive(SimpleObject)]
struct Satellite {
    ...
    first_spacecraft_landing_date: Option<NaiveDate>,
}
```

## Apollo Federation support

One of the purposes of `satellites-service` is to demonstrate how the distributed GraphQL [*entity*](https://www.apollographql.com/docs/federation/entities/) (`Planet`) can be resolved in two (or more) services and then accessed through Apollo Server.

`Planet` type was earlier defined in the `planets-service` in this way:

<div class="formalpara-title" markdown="1">

*[Definition of `Planet` type in `planets-service`](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
#[derive(Serialize, Deserialize)]
struct Planet {
    id: ID,
    name: String,
    planet_type: PlanetType,
}
```

Also, in `planets-service` `Planet` type is an entity:

<div class="formalpara-title" markdown="1">

*[`Planet` entity definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/planets-service/src/graphql.rs)*

</div>

``` rust
#[Object]
impl Query {
    #[graphql(entity)]
    async fn find_planet_by_id(&self, ctx: &Context<'_>, id: ID) -> Option<Planet> {
        find_planet_by_id_internal(ctx, id)
    }
}
```

`satellites-service` extends `Planet` entity by adding the `satellites` field to it:

<div class="formalpara-title" markdown="1">

*[Extension of `Planet` type in `satellites-service`](https://github.com/rkudryashov/graphql-rust-demo/blob/master/satellites-service/src/graphql.rs)*

</div>

``` rust
struct Planet {
    id: ID
}

#[Object(extends)]
impl Planet {
    #[graphql(external)]
    async fn id(&self) -> &ID {
        &self.id
    }

    async fn satellites(&self, ctx: &Context<'_>) -> Vec<Satellite> {
        let id = self.id.to_string().parse::<i32>().expect("Can't get id from String");
        repository::get_by_planet_id(id, &get_conn_from_ctx(ctx)).expect("Can't get satellites of planet")
            .iter()
            .map(|e| { Satellite::from(e) })
            .collect()
    }
}
```

You should also provide a lookup function for an extended type (here it just creates a new instance of `Planet`):

<div class="formalpara-title" markdown="1">

*[Lookup function for `Planet` type ](https://github.com/rkudryashov/graphql-rust-demo/blob/master/satellites-service/src/graphql.rs)*

</div>

``` rust
#[Object]
impl Query {

    #[graphql(entity)]
    async fn get_planet_by_id(&self, id: ID) -> Planet {
        Planet { id }
    }
}
```

Async-graphql generates two additional queries (`_service` and `_entities`) that will be used by Apollo Server. These queries are internal, i.e., they won’t be exposed by Apollo Server. Of course, a service with Apollo Federation support can still work independently.

## Apollo Server

Apollo Server and Apollo Federation allow achieving 2 main goals:

-   create a single endpoint to access GraphQL APIs provided by multiple services

-   create a single data graph from distributed entities

That is, even if you don’t use federated entities, it is more convenient for frontend developers to use a single endpoint rather than multiple endpoints.

There is another way for creating a single GraphQL schema, [schema stitching](https://www.graphql-tools.com/docs/schema-stitching/), but I haven’t used this approach.

This module includes the following sources:

<div class="formalpara-title" markdown="1">

*[Meta information and dependencies](https://github.com/rkudryashov/graphql-rust-demo/blob/master/apollo-server/package.json)*

</div>

``` json
{
  "name": "api-gateway",
  "main": "gateway.js",
  "scripts": {
    "start-gateway": "nodemon gateway.js"
  },
  "devDependencies": {
    "concurrently": "5.3.0",
    "nodemon": "2.0.6"
  },
  "dependencies": {
    "@apollo/gateway": "0.21.3",
    "apollo-server": "2.19.0",
    "graphql": "15.4.0"
  }
}
```

<div class="formalpara-title" markdown="1">

*[Apollo Server definition](https://github.com/rkudryashov/graphql-rust-demo/blob/master/apollo-server/gateway.js)*

</div>

``` javascript
const {ApolloServer} = require("apollo-server");
const {ApolloGateway, RemoteGraphQLDataSource} = require("@apollo/gateway");

class AuthenticatedDataSource extends RemoteGraphQLDataSource {
    willSendRequest({request, context}) {
        if (context.authHeaderValue) {
            request.http.headers.set('Authorization', context.authHeaderValue);
        }
    }
}

let node_env = process.env.NODE_ENV;

function get_service_url(service_name, port) {
    let host;
    switch (node_env) {
        case 'docker':
            host = service_name;
            break;
        case 'local': {
            host = 'localhost';
            break
        }
    }

    return "http://" + host + ":" + port;
}

const gateway = new ApolloGateway({
    serviceList: [
        {name: "planets-service", url: get_service_url("planets-service", 8001)},
        {name: "satellites-service", url: get_service_url("satellites-service", 8002)},
        {name: "auth-service", url: get_service_url("auth-service", 8003)},
    ],
    buildService({name, url}) {
        return new AuthenticatedDataSource({url});
    },
});

const server = new ApolloServer({
    gateway, subscriptions: false, context: ({req}) => ({
        authHeaderValue: req.headers.authorization
    })
});

server.listen({host: "0.0.0.0", port: 4000}).then(({url}) => {
    console.log(`🚀 Server ready at ${url}`);
});
```

If the source above can be simplified, feel free to contact me for change.

Authorization in `apollo-service` works as was described earlier for Rust services (you just need to specify `Authorization` header and its value).

An application written in any language or framework can be added as a downstream service to Apollo Server if it implements [Federation specification](https://www.apollographql.com/docs/federation/federation-spec/); a list of libraries that offer such support is available in the [documentation](https://www.apollographql.com/docs/federation/other-servers/).

While implementing this module, I encountered some limitations:

-   Apollo Gateway [doesn’t support](https://github.com/apollographql/apollo-server/issues/3357) subscriptions (but they still work in a standalone Rust GraphQL application)

-   a service trying to extend a GraphQL interface [requires knowledge of concrete implementations](https://github.com/apollographql/apollo-server/issues/2849)

## Interaction with a database

The persistence layer is implemented using PostgreSQL and Diesel. If you don’t use Docker locally, you should run `diesel setup` being in each service’s folder. This will create an empty database to which then will be applied migrations creating tables and inserting data.

## Project launch and API testing

As was mentioned before, for launching the project locally you have two options:

-   using Docker Compose ([docker-compose.yml](https://github.com/rkudryashov/graphql-rust-demo/blob/master/docker-compose.yml))

    In its turn, here you also have two options:

    -   development mode (using locally built images)

        `docker-compose up --build`

    -   production mode (using released images)

        `docker-compose -f docker-compose.yml up`

-   without Docker

    Start each Rust service using `cargo run`, then start Apollo Server:

    -   `cd` to the `apollo-server` folder

    -   define `NODE_ENV` variable, for example, `set NODE_ENV=local` (for Windows)

    -   `npm install`

    -   `npm run start-gateway`

A successful launch of `apollo-server` should look like this:

<div class="formalpara-title" markdown="1">

*Apollo Server startup log*

</div>

``` plaintext
[nodemon] 2.0.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node gateway.js`
Server ready at http://0.0.0.0:4000/
```

You can navigate to `http://localhost:4000` in your browser and use a built-in Playground IDE:

![playground](https://dev-to-uploads.s3.amazonaws.com/i/ljlhzbg8z0gmyc36bl1i.png)

Here you can execute queries, mutations, and subscriptions defined in downstream services. Also, each of those services has its own Playground IDE.

## Subscription test

To test whether the subscription works open two tabs of any GraphQL IDE; in the first subscribe as follows:

<div class="formalpara-title" markdown="1">

*Request for subscription*

</div>

``` graphql
subscription {
  latestPlanet {
    name
    type
  }
}
```

In the second specify `Authorization` header as was described above and perform mutation like this:

<div class="formalpara-title" markdown="1">

*Request for mutation*

</div>

``` graphql
mutation {
  createPlanet(
    planet: {
      name: "Pluto"
      type: DWARF_PLANET
      details: { meanRadius: "1188", mass: "1.303e22" }
    }
  ) {
    id
  }
}
```

The subscribed client will be notified of a planet creation:

![subscription demo](https://dev-to-uploads.s3.amazonaws.com/i/ktv645ccfviq73v5w4r1.gif)

## CI/CD

CI/CD is configured using GitHub Actions ([workflow](https://github.com/rkudryashov/graphql-rust-demo/blob/master/.github/workflows/workflow.yml)) that runs applications' tests, builds their Docker images, and deploys them on Google Cloud Platform.

You can take a look at the described API [here](http://graphql-rust.romankudryashov.com).

**Note:** On the "production" environment password is different from the one specified earlier to prevent changing initial data.

# Conclusion

In this article, I considered how to solve the most common problems that can arise while developing GraphQL API in Rust. Also, I showed how to combine APIs of Rust GraphQL microservices to provide a unified GraphQL interface; in such an architecture an entity can be distributed among several microservices. It is achieved by using Apollo Server, Apollo Federation, and Async-graphql library. The source code of the considered project is on [GitHub](https://github.com/rkudryashov/graphql-rust-demo). Feel free to contact me if you have found any mistakes in the article or the source code. Thanks for reading!

# Useful links

-   [graphql.org](https://graphql.org/)

-   [spec.graphql.org](https://spec.graphql.org/)

-   [graphql.org/learn/best-practices](https://graphql.org/learn/best-practices/)

-   [howtographql.com](https://www.howtographql.com/)

-   [Async-graphql](https://github.com/async-graphql/async-graphql)

-   [Async-graphql book](https://async-graphql.github.io/async-graphql/en/index.html)

-   [Awesome GraphQL](https://github.com/chentsulin/awesome-graphql)

-   [Public GraphQL APIs](https://github.com/APIs-guru/graphql-apis)

-   [Apollo Federation demo](https://github.com/apollographql/federation-demo)