Introduction

Async-graphql is a GraphQL server-side library implemented in Rust. It is fully compatible with the GraphQL specification and most of its extensions, and offers type safety and high performance.

You can define a Schema in Rust and procedural macros will automatically generate code for a GraphQL query. This library does not extend Rust's syntax, which means that Rustfmt can be used normally. I value this highly and it is one of the reasons why I developed Async-graphql.

Why do this?

I like GraphQL and Rust. I've been using Juniper, which solves the problem of implementing a GraphQL server with Rust. But Juniper had several problems, the most important of which is that it didn't support async/await at the time. So I decided to make this library for myself.

Benchmarks

Ensure that there is no CPU-heavy process in background!

cd benchmark
cargo bench

Now a HTML report is available at benchmark/target/criterion/report.

Quickstart

Add dependency libraries

[dependencies]
async-graphql = "4.0"
async-graphql-actix-web = "4.0" # If you need to integrate into actix-web
async-graphql-warp = "4.0" # If you need to integrate into warp
async-graphql-tide = "4.0" # If you need to integrate into tide

Write a Schema

The Schema of a GraphQL contains a required Query, an optional Mutation, and an optional Subscription. These object types are described using the structure of the Rust language. The field of the structure corresponds to the field of the GraphQL object.

Async-graphql implements the mapping of common data types to GraphQL types, such as i32, f64, Option<T>, Vec<T>, etc. Also, you can extend these base types, which are called scalars in the GraphQL.

Here is a simple example where we provide just one query that returns the sum of a and b.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    /// Returns the sum of a and b
    async fn add(&self, a: i32, b: i32) -> i32 {
        a + b
    }
}
}

Execute the query

In our example, there is only a Query without a Mutation or Subscription, so we create the Schema with EmptyMutation and EmptySubscription, and then call Schema::execute to execute the Query.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;
#[Object]
impl Query {
  async fn version(&self) -> &str { "1.0" }    
}
async fn other() {
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let res = schema.execute("{ add(a: 10, b: 20) }").await;
}
}

Output the query results as JSON

let json = serde_json::to_string(&res);

Web server integration

Please refer to https://github.com/async-graphql/examples.

Type System

Async-graphql implements conversions from GraphQL Objects to Rust structs, and it's easy to use.

SimpleObject

SimpleObject directly maps all the fields of a struct to GraphQL object. If you don't require automatic mapping of fields, see Object.

The example below defines an object MyObject which includes the fields a and b. c will be not mapped to GraphQL as it is labelled as #[graphql(skip)]


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

#[derive(SimpleObject)]
struct MyObject {
    /// Value a
    a: i32,

    /// Value b
    b: i32,

    #[graphql(skip)]
    c: i32,
}
}

User-defined resolvers

Sometimes most of the fields of a GraphQL object simply return the value of the structure member, but a few fields are calculated. In this case, the Object macro cannot be used unless you hand-write all the resolvers.

The ComplexObject macro works in conjunction with the SimpleObject macro. The SimpleObject derive macro defines the non-calculated fields, where as the ComplexObject macro let's you write user-defined resolvers for the calculated fields.

Resolvers added to ComplexObject adhere to the same rules as resolvers of Object.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
#[graphql(complex)] // NOTE: If you want the `ComplexObject` macro to take effect, this `complex` attribute is required.
struct MyObj {
    a: i32,
    b: i32,
}

#[ComplexObject]
impl MyObj {
    async fn c(&self) -> i32 {
        self.a + self.b
    }
}
}

Generic SimpleObjects

If you want to reuse an SimpleObject for other types, you can define a generic SimpleObject and specify how its concrete types should be implemented.

In the following example, two SimpleObject types are created:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct SomeType { a: i32 }
#[derive(SimpleObject)]
struct SomeOtherType { a: i32 }
#[derive(SimpleObject)]
#[graphql(concrete(name = "SomeName", params(SomeType)))]
#[graphql(concrete(name = "SomeOtherName", params(SomeOtherType)))]
pub struct SomeGenericObject<T: OutputType> {
    field1: Option<T>,
    field2: String
}
}

Note: Each generic parameter must implement OutputType, as shown above.

The schema generated is:

type SomeName {
  field1: SomeType
  field2: String!
}

type SomeOtherName {
  field1: SomeOtherType
  field2: String!
}

In your resolver method or field of another object, use as a normal generic type:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct SomeType { a: i32 }
#[derive(SimpleObject)]
struct SomeOtherType { a: i32 }
#[derive(SimpleObject)]
#[graphql(concrete(name = "SomeName", params(SomeType)))]
#[graphql(concrete(name = "SomeOtherName", params(SomeOtherType)))]
pub struct SomeGenericObject<T: OutputType> {
    field1: Option<T>,
    field2: String,
}
#[derive(SimpleObject)]
pub struct YetAnotherObject {
    a: SomeGenericObject<SomeType>,
    b: SomeGenericObject<SomeOtherType>,
}
}

You can pass multiple generic types to params(), separated by a comma.

Used for both input and output


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject, InputObject)]
#[graphql(input_name = "MyObjInput")] // Note: You must use the input_name attribute to define a new name for the input type, otherwise a runtime error will occur.
struct MyObj {
    a: i32,
    b: i32,
}
}

Object

Different from SimpleObject, Object must have a resolver defined for each field in its impl.

A resolver function has to be asynchronous. The first argument has to be &self, the second is an optional Context and it is followed by field arguments.

The resolver is used to get the value of the field. For example, you can query a database and return the result. The return type of the function is the type of the field. You can also return a async_graphql::Result to return an error if it occurs. The error message will then be sent as query result.

You may need access to global data in your query, for example a database connection pool. When creating your Schema, you can use SchemaBuilder::data to configure the global data, and Context::data to configure Context data. The following value_from_db function shows how to retrieve a database connection from Context.


#![allow(unused)]
fn main() {
extern crate async_graphql;
struct Data { pub name: String }
struct DbConn {}
impl DbConn {
  fn query_something(&self, id: i64) -> std::result::Result<Data, String> { Ok(Data {name:"".into()})}
}
struct DbPool {}
impl DbPool {
  fn take(&self) -> DbConn { DbConn {} }    
}
use async_graphql::*;

struct MyObject {
    value: i32,
}

#[Object]
impl MyObject {
    async fn value(&self) -> String {
        self.value.to_string()
    }

    async fn value_from_db(
        &self,
        ctx: &Context<'_>,
        #[graphql(desc = "Id of object")] id: i64
    ) -> Result<String> {
        let conn = ctx.data::<DbPool>()?.take();
        Ok(conn.query_something(id)?.name)
    }
}
}

Context

The main goal of Context is to acquire global data attached to Schema and also data related to the actual query being processed.

Store Data

Inside the Context you can put global data, like environment variables, db connection pool, whatever you may need in every query.

The data must implement Send and Sync.

You can request the data inside a query by just calling ctx.data::<TypeOfYourData>().

Note that if the return value of resolver function is borrowed from Context, you will need to explicitly state the lifetime of the argument.

The following example shows how to borrow data in Context.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    async fn borrow_from_context_data<'ctx>(
        &self,
        ctx: &Context<'ctx>
    ) -> Result<&'ctx String> {
        ctx.data::<String>()
    }
}
}

Schema data

You can put data inside the context at the creation of the schema, it's useful for data that do not change, like a connection pool.

An instance of how it would be written inside an application:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(Default,SimpleObject)]
struct Query { version: i32}
struct EnvStruct;
let env_struct = EnvStruct;
struct S3Object;
let s3_storage = S3Object;
struct DBConnection;
let db_core = DBConnection;
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription)
    .data(env_struct)
    .data(s3_storage)
    .data(db_core)
    .finish();
}

Request data

You can put data inside the context at the execution of the request, it's useful for authentication data for instance.

A little example with a warp route:


#![allow(unused)]
fn main() {
extern crate async_graphql;
extern crate async_graphql_warp;
extern crate warp;
use async_graphql::*;
use warp::{Filter, Reply};
use std::convert::Infallible;
#[derive(Default, SimpleObject)]
struct Query { name: String }
struct AuthInfo { pub token: Option<String> }
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription).finish();
let schema_filter = async_graphql_warp::graphql(schema);
let graphql_post = warp::post()
  .and(warp::path("graphql"))
  .and(warp::header::optional("Authorization"))
  .and(schema_filter)
  .and_then( |auth: Option<String>, (schema, mut request): (Schema<Query, EmptyMutation, EmptySubscription>, async_graphql::Request)| async move {
    // Do something to get auth data from the header
    let your_auth_data = AuthInfo { token: auth };
    let response = schema
      .execute(
        request
         .data(your_auth_data)
      ).await;

    Ok::<_, Infallible>(async_graphql_warp::GraphQLResponse::from(response))
  });
}

Headers

With the Context you can also insert and appends headers.


#![allow(unused)]
fn main() {
extern crate async_graphql;
extern crate http;
use ::http::header::ACCESS_CONTROL_ALLOW_ORIGIN;
use async_graphql::*;
struct Query;
#[Object]
impl Query {
    async fn greet(&self, ctx: &Context<'_>) -> String {
        // Headers can be inserted using the `http` constants
        let was_in_headers = ctx.insert_http_header(ACCESS_CONTROL_ALLOW_ORIGIN, "*");

        // They can also be inserted using &str
        let was_in_headers = ctx.insert_http_header("Custom-Header", "1234");

        // If multiple headers with the same key are `inserted` then the most recent
        // one overwrites the previous. If you want multiple headers for the same key, use
        // `append_http_header` for subsequent headers
        let was_in_headers = ctx.append_http_header("Custom-Header", "Hello World");

        String::from("Hello world")
    }
}
}

Selection / LookAhead

Sometimes you want to know what fields are requested in the subquery to optimize the processing of data. You can read fields across the query with ctx.field() which will give you a SelectionField which will allow you to navigate across the fields and subfields.

If you want to perform a search across the query or the subqueries, you do not have to do this by hand with the SelectionField, you can use the ctx.look_ahead() to perform a selection


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

#[derive(SimpleObject)]
struct Detail {
    c: i32,
    d: i32,
}

#[derive(SimpleObject)]
struct MyObj {
    a: i32,
    b: i32,
    detail: Detail,
}

struct Query;

#[Object]
impl Query {
    async fn obj(&self, ctx: &Context<'_>) -> MyObj {
        if ctx.look_ahead().field("a").exists() {
            // This is a query like `obj { a }`
        } else if ctx.look_ahead().field("detail").field("c").exists() {
            // This is a query like `obj { detail { c } }`
        } else {
            // This query doesn't have `a`
        }
        unimplemented!()
    }
}
}

Error handling

Resolve can return a Result, which has the following definition:

type Result<T> = std::result::Result<T, Error>;

Any Error that implements std::fmt::Display can be converted to Error and you can extend the error message.

The following example shows how to parse an input string to an integer. When parsing fails, it will return an error and attach an error message. See the Error Extensions section of this book for more details.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use std::num::ParseIntError;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    async fn parse_with_extensions(&self, input: String) -> Result<i32> {
        Ok("234a"
            .parse()
            .map_err(|err: ParseIntError| err.extend_with(|_, e| e.set("code", 400)))?)
    }
}
}

Errors in subscriptions

Errors can be returned from subscription resolvers as well, using a return type of the form:

async fn my_subscription_resolver(&self) -> impl Stream<Item = Result<MyItem, MyError>> { ... }

Note however that the MyError struct must have Clone implemented, due to the restrictions placed by the Subscription macro. One way to accomplish this is by creating a custom error type, with #[derive(Clone)], as seen here.

Merging Objects

Usually we can create multiple implementations for the same type in Rust, but due to the limitation of procedural macros, we can not create multiple Object implementations for the same type. For example, the following code will fail to compile.

#[Object]
impl Query {
    async fn users(&self) -> Vec<User> {
        todo!()
    }
}

#[Object]
impl Query {
    async fn movies(&self) -> Vec<Movie> {
        todo!()
    }
}

Instead, the #[derive(MergedObject)] macro allows you to split an object's resolvers across multiple modules or files by merging 2 or more #[Object] implementations into one.

Tip: Every #[Object] needs a unique name, even in a MergedObject, so make sure to give each object you're merging its own name.

Note: This works for queries and mutations. For subscriptions, see "Merging Subscriptions" below.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct User { a: i32 }
#[derive(SimpleObject)]
struct Movie { a: i32 }
#[derive(Default)]
struct UserQuery;

#[Object]
impl UserQuery {
    async fn users(&self) -> Vec<User> {
        todo!()
    }
}

#[derive(Default)]
struct MovieQuery;

#[Object]
impl MovieQuery {
    async fn movies(&self) -> Vec<Movie> {
        todo!()
    }
}

#[derive(MergedObject, Default)]
struct Query(UserQuery, MovieQuery);

let schema = Schema::new(
    Query::default(),
    EmptyMutation,
    EmptySubscription
);
}

⚠️ MergedObject cannot be used in Interface。

Merging Subscriptions

Along with MergedObject, you can derive MergedSubscription or use #[MergedSubscription] to merge separate #[Subscription] blocks.

Like merging Objects, each subscription block requires a unique name.

Example:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use futures_util::stream::{Stream};
#[derive(Default,SimpleObject)]
struct Query { a: i32 }
#[derive(Default)]
struct Subscription1;

#[Subscription]
impl Subscription1 {
    async fn events1(&self) -> impl Stream<Item = i32> {
        futures_util::stream::iter(0..10)
    }
}

#[derive(Default)]
struct Subscription2;

#[Subscription]
impl Subscription2 {
    async fn events2(&self) -> impl Stream<Item = i32> {
        futures_util::stream::iter(10..20)
    }
}

#[derive(MergedSubscription, Default)]
struct Subscription(Subscription1, Subscription2);

let schema = Schema::new(
    Query::default(),
    EmptyMutation,
    Subscription::default()
);
}

Derived fields

Sometimes two fields have the same query logic, but the output type is different. In async-graphql, you can create a derived field for it.

In the following example, you already have a duration_rfc2822 field outputting the time format in RFC2822 format, and then reuse it to derive a new date_rfc3339 field.


#![allow(unused)]
fn main() {
extern crate chrono;
use chrono::Utc;
extern crate async_graphql;
use async_graphql::*;
struct DateRFC3339(chrono::DateTime<Utc>);
struct DateRFC2822(chrono::DateTime<Utc>);

#[Scalar]
impl ScalarType for DateRFC3339 {
  fn parse(value: Value) -> InputValueResult<Self> { todo!() } 

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

#[Scalar]
impl ScalarType for DateRFC2822 {
  fn parse(value: Value) -> InputValueResult<Self> { todo!() } 

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

impl From<DateRFC2822> for DateRFC3339 {
    fn from(value: DateRFC2822) -> Self {
      DateRFC3339(value.0)
    }
}

struct Query;

#[Object]
impl Query {
    #[graphql(derived(name = "date_rfc3339", into = "DateRFC3339"))]
    async fn duration_rfc2822(&self, arg: String) -> DateRFC2822 {
        todo!()
    }
}
}

It will render a GraphQL like:

type Query {
	duration_rfc2822(arg: String): DateRFC2822!
	duration_rfc3339(arg: String): DateRFC3339!
}

Wrapper types

A derived field won't be able to manage everything easily: Rust's orphan rule requires that either the trait or the type for which you are implementing the trait must be defined in the same crate as the impl, so the following code cannot be compiled:

impl From<Vec<U>> for Vec<T> {
  ...
}

So you wouldn't be able to generate derived fields for existing wrapper type structures like Vec or Option. But when you implement a From<U> for T you should be able to derived a From<Vec<U>> for Vec<T> and a From<Option<U>> for Option<T>. We included a with parameter to help you define a function to call instead of using the Into trait implementation between wrapper structures.

Example


#![allow(unused)]
fn main() {
extern crate serde;
use serde::{Serialize, Deserialize};
extern crate async_graphql;
use async_graphql::*;
#[derive(Serialize, Deserialize, Clone)]
struct ValueDerived(String);

#[derive(Serialize, Deserialize, Clone)]
struct ValueDerived2(String);

scalar!(ValueDerived);
scalar!(ValueDerived2);

impl From<ValueDerived> for ValueDerived2 {
    fn from(value: ValueDerived) -> Self {
        ValueDerived2(value.0)
    }
}

fn option_to_option<T, U: From<T>>(value: Option<T>) -> Option<U> {
    value.map(|x| x.into())
}

#[derive(SimpleObject)]
struct TestObj {
    #[graphql(derived(owned, name = "value2", into = "Option<ValueDerived2>", with = "option_to_option"))]
    pub value1: Option<ValueDerived>,
}
}

Enum

It's easy to define an Enum, here we have an example:

Async-graphql will automatically change the name of each item to GraphQL's CONSTANT_CASE convention. You can use name to rename.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

/// One of the films in the Star Wars Trilogy
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum Episode {
    /// Released in 1977.
    NewHope,

    /// Released in 1980.
    Empire,

    /// Released in 1983.
    #[graphql(name="AAA")]
    Jedi,
}
}

Wrapping a remote enum

Rust's orphan rule requires that either the trait or the type for which you are implementing the trait must be defined in the same crate as the impl, so you cannot expose remote enumeration types to GraphQL. In order to provide an Enum type, a common workaround is to create a new enum that has parity with the existing, remote enum type.


#![allow(unused)]
fn main() {
extern crate async_graphql;
mod remote_crate { pub enum RemoteEnum { A, B, C } }
use async_graphql::*;

/// Provides parity with a remote enum type
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum LocalEnum {
    A,
    B,
    C,
}

/// Conversion interface from remote type to our local GraphQL enum type
impl From<remote_crate::RemoteEnum> for LocalEnum {
    fn from(e: remote_crate::RemoteEnum) -> Self {
        match e {
            remote_crate::RemoteEnum::A => Self::A,
            remote_crate::RemoteEnum::B => Self::B,
            remote_crate::RemoteEnum::C => Self::C,
        }
    }
}
}

The process is tedious and requires multiple steps to keep the local and remote enums in sync. Async_graphql provides a handy feature to generate the From<remote_crate::RemoteEnum> for LocalEnum as well as an opposite direction of From<LocalEnum> for remote_crate::RemoteEnum via an additional attribute after deriving Enum:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
mod remote_crate { pub enum RemoteEnum { A, B, C } }
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
#[graphql(remote = "remote_crate::RemoteEnum")]
enum LocalEnum {
    A,
    B,
    C,
}
}

Interface

Interface is used to abstract Objects with common fields. Async-graphql implements it as a wrapper. The wrapper will forward field resolution to the Object that implements this Interface. Therefore, the Object's fields' type and arguments must match with the Interface's.

Async-graphql implements auto conversion from Object to Interface, you only need to call Into::into.

Interface field names are transformed to camelCase for the schema definition. If you need e.g. a snake_cased GraphQL field name, you can use both the name and method attributes.

  • When name and method exist together, name is the GraphQL field name and the method is the resolver function name.
  • When only name exists, name.to_camel_case() is the GraphQL field name and the name is the resolver function name.

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Circle {
    radius: f32,
}

#[Object]
impl Circle {
    async fn area(&self) -> f32 {
        std::f32::consts::PI * self.radius * self.radius
    }

    async fn scale(&self, s: f32) -> Shape {
        Circle { radius: self.radius * s }.into()
    }

    #[graphql(name = "short_description")]
    async fn short_description(&self) -> String {
        "Circle".to_string()
    }
}

struct Square {
    width: f32,
}

#[Object]
impl Square {
    async fn area(&self) -> f32 {
        self.width * self.width
    }

    async fn scale(&self, s: f32) -> Shape {
        Square { width: self.width * s }.into()
    }

    #[graphql(name = "short_description")]
    async fn short_description(&self) -> String {
        "Square".to_string()
    }
}

#[derive(Interface)]
#[graphql(
    field(name = "area", type = "f32"),
    field(name = "scale", type = "Shape", arg(name = "s", type = "f32")),
    field(name = "short_description", method = "short_description", type = "String")
)]
enum Shape {
    Circle(Circle),
    Square(Square),
}
}

Register the interface manually

Async-graphql traverses and registers all directly or indirectly referenced types from Schema in the initialization phase. If an interface is not referenced, it will not exist in the registry, as in the following example , even if MyObject implements MyInterface, because MyInterface is not referenced in Schema, the MyInterface type will not exist in the registry.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(Interface)]
#[graphql(
    field(name = "name", type = "String"),
)]
enum MyInterface {
    MyObject(MyObject),
}

#[derive(SimpleObject)]
struct MyObject {
    name: String,
}

struct Query;

#[Object]
impl Query {
    async fn obj(&self) -> MyObject {
        todo!()
    }
}

type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
}

You need to manually register the MyInterface type when constructing the Schema:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(Interface)]
#[graphql(field(name = "name", type = "String"))]
enum MyInterface { MyObject(MyObject) }
#[derive(SimpleObject)]
struct MyObject { name: String, }
struct Query;
#[Object]
impl Query { async fn version(&self) -> &str { "1.0" } }

Schema::build(Query, EmptyMutation, EmptySubscription)
    .register_output_type::<MyInterface>()
    .finish();
}

Union

The definition of a Union is similar to an Interface, but with no fields allowed.. The implementation is quite similar for Async-graphql; from Async-graphql's perspective, Union is a subset of Interface.

The following example modified the definition of Interface a little bit and removed fields.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Circle {
    radius: f32,
}

#[Object]
impl Circle {
    async fn area(&self) -> f32 {
        std::f32::consts::PI * self.radius * self.radius
    }

    async fn scale(&self, s: f32) -> Shape {
        Circle { radius: self.radius * s }.into()
    }
}

struct Square {
    width: f32,
}

#[Object]
impl Square {
    async fn area(&self) -> f32 {
        self.width * self.width
    }

    async fn scale(&self, s: f32) -> Shape {
        Square { width: self.width * s }.into()
    }
}

#[derive(Union)]
enum Shape {
    Circle(Circle),
    Square(Square),
}
}

Flattening nested unions

A restriction in GraphQL is the inability to create a union type out of other union types. All members must be Object. To support nested unions, we can "flatten" members that are unions, bringing their members up into the parent union. This is done by applying #[graphql(flatten)] on each member we want to flatten.


#![allow(unused)]
fn main() {
extern crate async_graphql;
#[derive(async_graphql::Union)]
pub enum TopLevelUnion {
    A(A),

    // Will fail to compile unless we flatten the union member
    #[graphql(flatten)]
    B(B),
}

#[derive(async_graphql::SimpleObject)]
pub struct A {
    a: i32,
    // ...
}

#[derive(async_graphql::Union)]
pub enum B {
    C(C),
    D(D),
}

#[derive(async_graphql::SimpleObject)]
pub struct C {
    c: i32,
    // ...
}

#[derive(async_graphql::SimpleObject)]
pub struct D {
    d: i32,
    // ...
}
}

The above example transforms the top-level union into this equivalent:


#![allow(unused)]
fn main() {
extern crate async_graphql;
#[derive(async_graphql::SimpleObject)]
struct A { a: i32 }
#[derive(async_graphql::SimpleObject)]
struct C { c: i32 }
#[derive(async_graphql::SimpleObject)]
struct D { d: i32 }
#[derive(async_graphql::Union)]
pub enum TopLevelUnion {
    A(A),
    C(C),
    D(D),
}
}

InputObject

You can use an Object as an argument, and GraphQL calls it an InputObject.

The definition of InputObject is similar to SimpleObject, but SimpleObject can only be used as output and InputObject can only be used as input.

You can add optional #[graphql] attributes to add descriptions or rename the field.


#![allow(unused)]
fn main() {
extern crate async_graphql;
#[derive(SimpleObject)]
struct User { a: i32 }
use async_graphql::*;

#[derive(InputObject)]
struct Coordinate {
    latitude: f64,
    longitude: f64
}

struct Mutation;

#[Object]
impl Mutation {
    async fn users_at_location(&self, coordinate: Coordinate, radius: f64) -> Vec<User> {
        // Writes coordination to database.
        // ...
      todo!()
    }
}
}

Generic InputObjects

If you want to reuse an InputObject for other types, you can define a generic InputObject and specify how its concrete types should be implemented.

In the following example, two InputObject types are created:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(InputObject)]
struct SomeType { a: i32 }
#[derive(InputObject)]
struct SomeOtherType { a: i32 }
#[derive(InputObject)]
#[graphql(concrete(name = "SomeName", params(SomeType)))]
#[graphql(concrete(name = "SomeOtherName", params(SomeOtherType)))]
pub struct SomeGenericInput<T: InputType> {
    field1: Option<T>,
    field2: String
}
}

Note: Each generic parameter must implement InputType, as shown above.

The schema generated is:

input SomeName {
  field1: SomeType
  field2: String!
}

input SomeOtherName {
  field1: SomeOtherType
  field2: String!
}

In your resolver method or field of another input object, use as a normal generic type:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(InputObject)]
struct SomeType { a: i32 }
#[derive(InputObject)]
struct SomeOtherType { a: i32 }
#[derive(InputObject)]
#[graphql(concrete(name = "SomeName", params(SomeType)))]
#[graphql(concrete(name = "SomeOtherName", params(SomeOtherType)))]
pub struct SomeGenericInput<T: InputType> {
    field1: Option<T>,
    field2: String
}
#[derive(InputObject)]
pub struct YetAnotherInput {
    a: SomeGenericInput<SomeType>,
    b: SomeGenericInput<SomeOtherType>,
}
}

You can pass multiple generic types to params(), separated by a comma.

OneofObject

A OneofObject is a special type of InputObject, in which only one of its fields must be set and is not-null. It is especially useful when you want a user to be able to choose between several potential input types.

This feature is still an RFC and therefore not yet officially part of the GraphQL spec, but Async-graphql already supports it!


#![allow(unused)]
fn main() {
extern crate async_graphql;
#[derive(SimpleObject)]
struct User { a: i32 }
use async_graphql::*;

#[derive(OneofObject)]
enum UserBy {
    Email(String),
    RegistrationNumber(i64),
    Address(Address)
}

#[derive(InputObject)]
struct Address {
    street: String,
    house_number: String,
    city: String,
    zip: String,
}

struct Query {}

#[Object]
impl Query {
    async fn search_users(&self, by: Vec<UserBy>) -> Vec<User> {
        // ... Searches and returns a list of users ...
        todo!()
    }
}
}

As you can see, a OneofObject is represented by an enum in which each variant contains another InputType. This means that you can use InputObject as variant too.

Default value

You can define default values for input value types. Below are some examples.

Object field


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

fn my_default() -> i32 {
    30
}

#[Object]
impl Query {
    // The default value of the value parameter is 0, it will call i32::default()
    async fn test1(&self, #[graphql(default)] value: i32) -> i32 { todo!() }

    // The default value of the value parameter is 10
    async fn test2(&self, #[graphql(default = 10)] value: i32) -> i32 { todo!() }
    
    // The default value of the value parameter uses the return result of the my_default function, the value is 30.
    async fn test3(&self, #[graphql(default_with = "my_default()")] value: i32) -> i32 { todo!() }
}
}

Interface field


#![allow(unused)]
fn main() {
extern crate async_graphql;
fn my_default() -> i32 { 5 }
struct MyObj;
#[Object]
impl MyObj {
   async fn test1(&self, value: i32) -> i32 { todo!() }
   async fn test2(&self, value: i32) -> i32 { todo!() }
   async fn test3(&self, value: i32) -> i32 { todo!() }
}
use async_graphql::*;

#[derive(Interface)]
#[graphql(
    field(name = "test1", type = "i32", arg(name = "value", type = "i32", default)),
    field(name = "test2", type = "i32", arg(name = "value", type = "i32", default = 10)),
    field(name = "test3", type = "i32", arg(name = "value", type = "i32", default_with = "my_default()")),
)]
enum MyInterface {
    MyObj(MyObj),
}
}

Input object field


#![allow(unused)]
fn main() {
extern crate async_graphql;
fn my_default() -> i32 { 5 }
use async_graphql::*;

#[derive(InputObject)]
struct MyInputObject {
    #[graphql(default)]
    value1: i32,

    #[graphql(default = 10)]
    value2: i32,

    #[graphql(default_with = "my_default()")]
    value3: i32,
}
}

Schema

After defining the basic types, you need to define a schema to combine them. The schema consists of three types: a query object, a mutation object, and a subscription object, where the mutation object and subscription object are optional.

When the schema is created, Async-graphql will traverse all object graphs and register all types. This means that if a GraphQL object is defined but never referenced, this object will not be exposed in the schema.

Query and Mutation

Query root object

The query root object is a GraphQL object with a definition similar to other objects. Resolver functions for all fields of the query object are executed concurrently.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct User { a: i32 }

struct Query;

#[Object]
impl Query {
    async fn user(&self, username: String) -> Result<Option<User>> {
        // Look up users from the database
       todo!()
    }
}

}

Mutation root object

The mutation root object is also a GraphQL object, but it executes sequentially. One mutation following from another will only be executed only after the first mutation is completed.

The following mutation root object provides an example of user registration and login:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Mutation;

#[Object]
impl Mutation {
    async fn signup(&self, username: String, password: String) -> Result<bool> {
        // User signup
       todo!()
}

    async fn login(&self, username: String, password: String) -> Result<String> {
        // User login (generate token)
       todo!()
    }
}
}

Subscription

The definition of the subscription root object is slightly different from other root objects. Its resolver function always returns a Stream or Result<Stream>, and the field parameters are usually used as data filtering conditions.

The following example subscribes to an integer stream, which generates one integer per second. The parameter step specifies the integer step size with a default of 1.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use std::time::Duration;
use async_graphql::futures_util::stream::Stream;
use async_graphql::futures_util::StreamExt;
extern crate tokio_stream;
extern crate tokio;
use async_graphql::*;

struct Subscription;

#[Subscription]
impl Subscription {
    async fn integers(&self, #[graphql(default = 1)] step: i32) -> impl Stream<Item = i32> {
        let mut value = 0;
        tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(Duration::from_secs(1)))
            .map(move |_| {
                value += step;
                value
            })
    }
}
}

SDL Export

You can export your schema in Schema Definition Language (SDL) by using the Schema::sdl() method.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    async fn add(&self, u: i32, v: i32) -> i32 {
        u + v
    }
}

let schema = Schema::build(Query, EmptyMutation, EmptySubscription).finish();
    
// Print the schema in SDL format
println!("{}", &schema.sdl());
}

Utilities

Field Guard

You can define a guard for the fields of Object, SimpleObject, ComplexObject and Subscription, it will be executed before calling the resolver function, and an error will be returned if it fails.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(Eq, PartialEq, Copy, Clone)]
enum Role {
    Admin,
    Guest,
}

struct RoleGuard {
    role: Role,
}

impl RoleGuard {
    fn new(role: Role) -> Self {
        Self { 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())
        }
    }
}
}

Use it with the guard attribute:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(Eq, PartialEq, Copy, Clone)]
enum Role { Admin, Guest, }
struct RoleGuard { role: Role, }
impl RoleGuard { fn new(role: Role) -> Self { Self { role } } }
#[async_trait::async_trait]
impl Guard for RoleGuard { async fn check(&self, ctx: &Context<'_>) -> Result<()> { todo!() } }
#[derive(SimpleObject)]
struct Query {
    /// Only allow Admin
    #[graphql(guard = "RoleGuard::new(Role::Admin)")]
    value1: i32,
    /// Allow Admin or Guest
    #[graphql(guard = "RoleGuard::new(Role::Admin).or(RoleGuard::new(Role::Guest))")]
    value2: i32,
}
}

Use parameter value

Sometimes guards need to use field parameters, you need to pass the parameter value when creating the guard like this:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct EqGuard {
    expect: i32,
    actual: i32,
}

impl EqGuard {
    fn new(expect: i32, actual: i32) -> Self {
        Self { expect, actual }
    }
}

#[async_trait::async_trait]
impl Guard for EqGuard {
    async fn check(&self, _ctx: &Context<'_>) -> Result<()> {
        if self.expect != self.actual {
            Err("Forbidden".into())
        } else {
            Ok(())
        }
    }
}

struct Query;

#[Object]
impl Query {
    #[graphql(guard = "EqGuard::new(100, value)")]
    async fn get(&self, value: i32) -> i32 {
        value
    }
}
}

Input value validators

Async-graphql has some common validators built-in, you can use them on the parameters of object fields or on the fields of InputObject.

  • maximum=N the number cannot be greater than N.
  • minimum=N the number cannot be less than N.
  • multiple_of=N the number must be a multiple of N.
  • max_items=N the length of the list cannot be greater than N.
  • min_items=N the length of the list cannot be less than N.
  • max_length=N the length of the string cannot be greater than N.
  • min_length=N the length of the string cannot be less than N.
  • chars_max_length=N the count of the unicode chars cannot be greater than N.
  • chars_min_length=N the count of the unicode chars cannot be less than N.
  • email is valid email.
  • url is valid url.
  • ip is valid ip address.
  • regex=RE is match for the regex.

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    /// The length of the name must be greater than or equal to 5 and less than or equal to 10.
    async fn input(&self, #[graphql(validator(min_length = 5, max_length = 10))] name: String) -> Result<i32> {
        todo!()
    }
}
}

Check every member of the list

You can enable the list attribute, and the validator will check all members in list:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    async fn input(&self, #[graphql(validator(list, max_length = 10))] names: Vec<String>) -> Result<i32> {
       todo!()
    }
}
}

Custom validator


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct MyValidator {
    expect: i32,
}

impl MyValidator {
    pub fn new(n: i32) -> Self {
        MyValidator { expect: n }
    }
}

impl CustomValidator<i32> for MyValidator {
    fn check(&self, value: &i32) -> Result<(), String> {
        if *value == self.expect {
            Ok(())
        } else {
            Err(format!("expect 100, actual {}", value))
        }
    }
}

struct Query;

#[Object]
impl Query {
    /// n must be equal to 100
    async fn value(
        &self,
        #[graphql(validator(custom = "MyValidator::new(100)"))] n: i32,
    ) -> i32 {
        n
    }
}
}

Cache control

Production environments often rely on caching to improve performance.

A GraphQL query will call multiple resolver functions and each resolver can have a different cache definition. Some may cache for a few seconds, some may cache for a few hours, some may be the same for all users, and some may be different for each session.

Async-graphql provides a mechanism that allows you to define the cache time and scope for each resolver.

You can define cache parameters on the object or on its fields. The following example shows two uses of cache control parameters.

You can use max_age parameters to control the age of the cache (in seconds), and you can also use public and private to control the scope of the cache. When you do not specify it, the scope will default to public.

when querying multiple resolvers, the results of all cache control parameters will be combined and the max_age minimum value will be taken. If the scope of any object or field is private, the result will be private.

We can use QueryResponse to get a merged cache control result from a query result, and call CacheControl::value to get the corresponding HTTP header.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object(cache_control(max_age = 60))]
impl Query {
    #[graphql(cache_control(max_age = 30))]
    async fn value1(&self) -> i32 {
        1
    }

    #[graphql(cache_control(private))]
    async fn value2(&self) -> i32 {
        2
    }

    async fn value3(&self) -> i32 {
        3
    }
}
}

The following are different queries corresponding to different cache control results:

# max_age=30
{ value1 }
# max_age=30, private
{ value1 value2 }
# max_age=60
{ value3 }

Cursor connections

Relay's cursor connection specification is designed to provide a consistent method for query paging. For more details on the specification see the GraphQL Cursor Connections Specification

Defining a cursor connection in async-graphql is very simple, you just call the connection::query function and query data in the closure.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::types::connection::*;

struct Query;

#[Object]
impl Query {
    async fn numbers(&self,
        after: Option<String>,
        before: Option<String>,
        first: Option<i32>,
        last: Option<i32>,
    ) -> Result<Connection<usize, i32, EmptyFields, EmptyFields>> {
        query(after, before, first, last, |after, before, first, last| async move {
            let mut start = after.map(|after| after + 1).unwrap_or(0);
            let mut end = before.unwrap_or(10000);
            if let Some(first) = first {
                end = (start + first).min(end);
            }
            if let Some(last) = last {
                start = if last > end - start {
                     end
                } else {
                    end - last
                };
            }
            let mut connection = Connection::new(start > 0, end < 10000);
            connection.edges.extend(
                (start..end).into_iter().map(|n|
                    Edge::with_additional_fields(n, n as i32, EmptyFields)
            ));
            Ok::<_, async_graphql::Error>(connection)
        }).await
    }
}

}

Error extensions

To quote the graphql-spec:

GraphQL services may provide an additional entry to errors with key extensions. This entry, if set, must have a map as its value. This entry is reserved for implementer to add additional information to errors however they see fit, and there are no additional restrictions on its contents.

Example

I would recommend on checking out this async-graphql example as a quickstart.

General Concept

In async-graphql all user-facing errors are cast to the Error type which by default provides the error message exposed by std::fmt::Display. However, Error actually provides an additional information that can extend the error.

A resolver looks like this:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query {
async fn parse_with_extensions(&self) -> Result<i32, Error> {
    Err(Error::new("MyMessage").extend_with(|_, e| e.set("details", "CAN_NOT_FETCH")))
}
}
}

may then return a response like this:

{
  "errors": [
    {
      "message": "MyMessage",
      "locations": [ ... ],
      "path": [ ... ],
      "extensions": {
        "details": "CAN_NOT_FETCH",
      }
    }
  ]
}

ErrorExtensions

Constructing new Errors by hand quickly becomes tedious. That is why async-graphql provides two convenience traits for casting your errors to the appropriate Error with extensions.

The easiest way to provide extensions to any error is by calling extend_with on the error. This will on the fly convert any error into a Error with the given extension.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
use std::num::ParseIntError;
#[Object]
impl Query {
async fn parse_with_extensions(&self) -> Result<i32> {
     Ok("234a"
         .parse()
         .map_err(|err: ParseIntError| err.extend_with(|_err, e| e.set("code", 404)))?)
}
}
}

Implementing ErrorExtensions for custom errors.

If you find yourself attaching extensions to your errors all over the place you might want to consider implementing the trait on your custom error type directly.


#![allow(unused)]
fn main() {
extern crate async_graphql;
extern crate thiserror;
use async_graphql::*;
#[derive(Debug, thiserror::Error)]
pub enum MyError {
    #[error("Could not find resource")]
    NotFound,

    #[error("ServerError")]
    ServerError(String),

    #[error("No Extensions")]
    ErrorWithoutExtensions,
}

impl ErrorExtensions for MyError {
    // lets define our base extensions
    fn extend(&self) -> Error {
        Error::new(format!("{}", self)).extend_with(|err, e| 
            match self {
              MyError::NotFound => e.set("code", "NOT_FOUND"),
              MyError::ServerError(reason) => e.set("reason", reason.clone()),
              MyError::ErrorWithoutExtensions => {}
          })
    }
}
}

This way you only need to call extend on your error to deliver the error message alongside the provided extensions. Or further extend your error through extend_with.


#![allow(unused)]
fn main() {
extern crate async_graphql;
extern crate thiserror;
use async_graphql::*;
#[derive(Debug, thiserror::Error)]
pub enum MyError {
    #[error("Could not find resource")]
    NotFound,

    #[error("ServerError")]
    ServerError(String),

    #[error("No Extensions")]
    ErrorWithoutExtensions,
}
struct Query;
#[Object]
impl Query {
async fn parse_with_extensions_result(&self) -> Result<i32> {
    // Err(MyError::NotFound.extend())
    // OR
    Err(MyError::NotFound.extend_with(|_, e| e.set("on_the_fly", "some_more_info")))
}
}
}
{
  "errors": [
    {
      "message": "NotFound",
      "locations": [ ... ],
      "path": [ ... ],
      "extensions": {
        "code": "NOT_FOUND",
        "on_the_fly": "some_more_info"
      }
    }
  ]
}

ResultExt

This trait enables you to call extend_err directly on results. So the above code becomes less verbose.

// @todo figure out why this example does not compile!
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query {
async fn parse_with_extensions(&self) -> Result<i32> {
     Ok("234a"
         .parse()
         .extend_err(|_, e| e.set("code", 404))?)
}
}

Chained extensions

Since ErrorExtensions and ResultExt are implemented for any type &E where E: std::fmt::Display we can chain the extension together.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query {
async fn parse_with_extensions(&self) -> Result<i32> {
    match "234a".parse() {
        Ok(n) => Ok(n),
        Err(e) => Err(e
            .extend_with(|_, e| e.set("code", 404))
            .extend_with(|_, e| e.set("details", "some more info.."))
            // keys may also overwrite previous keys...
            .extend_with(|_, e| e.set("code", 500))),
    }
}
}
}

Expected response:

{
  "errors": [
    {
      "message": "MyMessage",
      "locations": [ ... ],
      "path": [ ... ],
      "extensions": {
      	"details": "some more info...",
        "code": 500,
      }
    }
  ]
}

Pitfalls

Rust does not provide stable trait specialization yet. That is why ErrorExtensions is actually implemented for &E where E: std::fmt::Display instead of E: std::fmt::Display. Some specialization is provided through Autoref-based stable specialization. The disadvantage is that the below code does NOT compile:

async fn parse_with_extensions_result(&self) -> Result<i32> {
    // the trait `error::ErrorExtensions` is not implemented
    // for `std::num::ParseIntError`
    "234a".parse().extend_err(|_, e| e.set("code", 404))
}

however this does:

async fn parse_with_extensions_result(&self) -> Result<i32> {
    // does work because ErrorExtensions is implemented for &ParseIntError
    "234a"
      .parse()
      .map_err(|ref e: ParseIntError| e.extend_with(|_, e| e.set("code", 404)))
}

Apollo Tracing

Apollo Tracing provides performance analysis results for each step of query. This is an extension to Schema, and the performance analysis results are stored in QueryResponse.

To enable the Apollo Tracing extension, add the extension when the Schema is created.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::ApolloTracing;

struct Query;
#[Object]
impl Query { async fn version(&self) -> &str { "1.0" } }

let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
    .extension(ApolloTracing) // Enable ApolloTracing extension
    .finish();
}

Query complexity and depth

⚠️GraphQL provides a powerful way to query your data, but putting great power in the hands of your API clients also exposes you to a risk of denial of service attacks. You can mitigate that risk with Async-graphql by limiting the complexity and depth of the queries you allow.

Expensive Queries

Consider a schema that allows listing blog posts. Each blog post is also related to other posts.

type Query {
	posts(count: Int = 10): [Post!]!
}

type Post {
	title: String!
	text: String!
	related(count: Int = 10): [Post!]!
}

It’s not too hard to craft a query that will cause a very large response:

{
    posts(count: 100) {
        related(count: 100) {
            related(count: 100) {
                related(count: 100) {
                    title
                }
            }
        }
    }
}

The size of the response increases exponentially with every other level of the related field. Fortunately, Async-graphql provides a way to prevent such queries.

Limiting Query depth

The depth is the number of nesting levels of the field, and the following is a query with a depth of 3.

{
    a {
        b {
            c
        }
    }
}

You can limit the depth when creating Schema. If the query exceeds this limit, an error will occur and the message Query is nested too deep will be returned.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query { async fn version(&self) -> &str { "1.0" } }
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
    .limit_depth(5) // Limit the maximum depth to 5
    .finish();
}

Limiting Query complexity

The complexity is the number of fields in the query. The default complexity of each field is 1. Below is a query with a complexity of 6.

{
    a b c {
        d {
            e f
        }
    }
}

You can limit the complexity when creating the Schema. If the query exceeds this limit, an error will occur and Query is too complex will be returned.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query { async fn version(&self) -> &str { "1.0" } }
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
    .limit_complexity(5) // Limit the maximum complexity to 5
    .finish();
}

Custom Complexity Calculation

There are two ways to customize the complexity for non-list type and list type fields.

In the following code, the complexity of the value field is 5. The complexity of the values field is count * child_complexity, child_complexity is a special variable that represents the complexity of the subquery, and count is the parameter of the field, used to calculate the complexity of the values field, and the type of the return value must be usize.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;

#[Object]
impl Query {
    #[graphql(complexity = 5)]
    async fn value(&self) -> i32 {
        todo!()
    }

    #[graphql(complexity = "count * child_complexity")]
    async fn values(&self, count: usize) -> i32 {
        todo!()
    }
}
}

Note: The complexity calculation is done in the validation phase and not the execution phase, so you don't have to worry about partial execution of over-limit queries.

Hide content in introspection

By default, all types and fields are visible in introspection. But maybe you want to hide some content according to different users to avoid unnecessary misunderstandings. You can add the visible attribute to the type or field to do it.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

#[derive(SimpleObject)]
struct MyObj {
    // This field will be visible in introspection.
    a: i32,

    // This field is always hidden in introspection.
    #[graphql(visible = false)]
    b: i32,

    // This field calls the `is_admin` function, which 
    // is visible if the return value is `true`.
    #[graphql(visible = "is_admin")]
    c: i32,
}

#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum MyEnum {
    // This item will be visible in introspection.
    A,

    // This item is always hidden in introspection.
    #[graphql(visible = false)]
    B,

    // This item calls the `is_admin` function, which 
    // is visible if the return value is `true`.
    #[graphql(visible = "is_admin")]
    C,
}

struct IsAdmin(bool);

fn is_admin(ctx: &Context<'_>) -> bool {
    ctx.data_unchecked::<IsAdmin>().0
}

}

Extensions

async-graphql has the capability to be extended with extensions without having to modify the original source code. A lot of features can be added this way, and a lot of extensions already exists.

How extensions are defined

An async-graphql extension is defined by implementing the trait Extension associated. The Extension trait allow you to insert custom code to some several steps used to respond to GraphQL's queries through async-graphql. With Extensions, your application can hook into the GraphQL's requests lifecycle to add behaviors about incoming requests or outgoing response.

Extensions are a lot like middleware from other frameworks, be careful when using those: when you use an extension it'll be run for every GraphQL requests.

Across every step, you'll have the ExtensionContext supplied with data about your current request execution. Feel free to check how it's constructed in the code, documentation about it will soon come.

A word about middleware

For those who don't know, let's dig deeper into what is a middleware:

async fn middleware(&self, ctx: &ExtensionContext<'_>, next: NextMiddleware<'_>) -> MiddlewareResult {
  // Logic to your middleware.

  /*
   * Final step to your middleware, we call the next function which will trigger
   * the execution of the next middleware. It's like a `callback` in JavaScript.
   */
  next.run(ctx).await
}

As you have seen, a Middleware is only a function calling the next function at the end, but we could also do a middleware with the next.run function at the start. This is where it's becoming tricky: depending on where you put your logics and where is the next.run call, your logic won't have the same execution order.

Depending on your logic code, you'll want to process it before or after the next.run call. If you need more information about middlewares, there are a lot of things in the web.

Processing of a query

There are several steps to go to process a query to completion, you'll be able to create extension based on these hooks.

request

First, when we receive a request, if it's not a subscription, the first function to be called will be request, it's the first step, it's the function called at the incoming request, and it's also the function which will output the response to the user.

Default implementation for request:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response {
    next.run(ctx).await
}
}
}

Depending on where you put your logic code, it'll be executed at the beginning or at the ending of the query being processed.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response {
    // The code here will be run before the prepare_request is executed.
    let result = next.run(ctx).await;
    // The code after the completion of this future will be after the processing, just before sending the result to the user.
    result
}
}
}

prepare_request

Just after the request, we will have the prepare_request lifecycle, which will be hooked.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
async fn prepare_request(
    &self,
    ctx: &ExtensionContext<'_>,
    request: Request,
    next: NextPrepareRequest<'_>,
) -> ServerResult<Request> {
    // The code here will be un before the prepare_request is executed, just after the request lifecycle hook.
    let result = next.run(ctx, request).await;
    // The code here will be run just after the prepare_request
    result
}
}
}

parse_query

The parse_query will create a GraphQL ExecutableDocument on your query, it'll check if the query is valid for the GraphQL Spec. Usually the implemented spec in async-graphql tends to be the last stable one (October2021).


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
use async_graphql::parser::types::ExecutableDocument;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
/// Called at parse query.
async fn parse_query(
    &self,
    ctx: &ExtensionContext<'_>,
    // The raw query
    query: &str,
    // The variables
    variables: &Variables,
    next: NextParseQuery<'_>,
) -> ServerResult<ExecutableDocument> {
    next.run(ctx, query, variables).await
}
}
}

validation

The validation step will check (depending on your validation_mode) rules the query should abide to and give the client data about why the query is not valid.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
/// Called at validation query.
async fn validation(
  &self,
  ctx: &ExtensionContext<'_>,
  next: NextValidation<'_>,
) -> Result<ValidationResult, Vec<ServerError>> {
  next.run(ctx).await
}
}
}

execute

The execution step is a huge one, it'll start the execution of the query by calling each resolver concurrently for a Query and serially for a Mutation.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
/// Called at execute query.
async fn execute(
    &self,
    ctx: &ExtensionContext<'_>,
    operation_name: Option<&str>,
    next: NextExecute<'_>,
) -> Response {
    // Before starting resolving the whole query
    let result = next.run(ctx, operation_name).await;
    // After resolving the whole query
    result
}
}
}

resolve

The resolve step is launched for each field.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware { 
/// Called at resolve field.
async fn resolve(
    &self,
    ctx: &ExtensionContext<'_>,
    info: ResolveInfo<'_>,
    next: NextResolve<'_>,
) -> ServerResult<Option<Value>> {
    // Logic before resolving the field
    let result = next.run(ctx, info).await;
    // Logic after resolving the field
    result
}
}
}

subscribe

The subscribe lifecycle has the same behavior as the request but for a Subscritpion.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
use futures_util::stream::BoxStream;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
/// Called at subscribe request.
fn subscribe<'s>(
    &self,
    ctx: &ExtensionContext<'_>,
    stream: BoxStream<'s, Response>,
    next: NextSubscribe<'_>,
) -> BoxStream<'s, Response> {
    next.run(ctx, stream)
}
}
}

Extensions available

There are a lot of available extensions in the async-graphql to empower your GraphQL Server, some of these documentations are documented here.

Analyzer

Available in the repository

The analyzer extension will output a field containing complexity and depth in the response extension field of each query.

Apollo Persisted Queries

Available in the repository

To improve network performance for large queries, you can enable this Persisted Queries extension. With this extension enabled, each unique query is associated to a unique identifier, so clients can send this identifier instead of the corresponding query string to reduce requests sizes.

This extension doesn't force you to use some cache strategy, you can choose the caching strategy you want, you'll just have to implement the CacheStorage trait:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[async_trait::async_trait]
pub trait CacheStorage: Send + Sync + Clone + 'static {
    /// Load the query by `key`.
    async fn get(&self, key: String) -> Option<String>;
    /// Save the query by `key`.
    async fn set(&self, key: String, query: String);
}
}

References: Apollo doc - Persisted Queries

Apollo Tracing

Available in the repository

Apollo Tracing is an extension which includes analytics data for your queries. This extension works to follow the old and now deprecated Apollo Tracing Spec. If you want to check the newer Apollo Reporting Protocol, it's implemented by async-graphql Apollo studio extension for Apollo Studio.

Apollo Studio

Available at async-graphql/async_graphql_apollo_studio_extension

Apollo Studio is a cloud platform that helps you build, validate, and secure your organization's graph (description from the official documentation). It's a service allowing you to monitor & work with your team around your GraphQL Schema. async-graphql provides an extension implementing the official Apollo Specification available at async-graphql-extension-apollo-tracing and Crates.io.

Logger

Available in the repository

Logger is a simple extension allowing you to add some logging feature to async-graphql. It's also a good example to learn how to create your own extension.

OpenTelemetry

Available in the repository

OpenTelemetry is an extension providing an integration with the opentelemetry crate to allow your application to capture distributed traces and metrics from async-grraphql.

Tracing

Available in the repository

Tracing is a simple extension allowing you to add some tracing feature to async-graphql. A little like the Logger extension.

Integrations

Async-graphql supports several common Rust web servers.

Even if the server you are currently using is not in the above list, it is quite simple to implement similar functionality yourself.

Poem

Request example


#![allow(unused)]
fn main() {
extern crate async_graphql_poem;
extern crate async_graphql;
extern crate poem;
use async_graphql::*;
#[derive(Default, SimpleObject)]
struct Query { a: i32 }
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription).finish();
use poem::Route;
use async_graphql_poem::GraphQL;

let app = Route::new()
    .at("/ws", GraphQL::new(schema));
}

Subscription example


#![allow(unused)]
fn main() {
extern crate async_graphql_poem;
extern crate async_graphql;
extern crate poem;
use async_graphql::*;
#[derive(Default, SimpleObject)]
struct Query { a: i32 }
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription).finish();
use poem::{get, Route};
use async_graphql_poem::GraphQLSubscription;

let app = Route::new()
    .at("/ws", get(GraphQLSubscription::new(schema)));
}

More examples

https://github.com/async-graphql/examples/tree/master/poem

Warp

For Async-graphql-warp, two Filter integrations are provided: graphql and graphql_subscription.

The graphql filter is used for execution Query and Mutation requests. It extracts GraphQL request and outputs async_graphql::Schema and async_graphql::Request. You can combine other filters later, or directly call Schema::execute to execute the query.

graphql_subscription is used to implement WebSocket subscriptions. It outputs warp::Reply.

Request example


#![allow(unused)]
fn main() {
extern crate async_graphql_warp;
extern crate async_graphql;
extern crate warp;
use async_graphql::*;
use std::convert::Infallible;
use warp::Filter;
struct QueryRoot;
#[Object]
impl QueryRoot { async fn version(&self) -> &str { "1.0" } }
async fn other() {
type MySchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>;

let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
let filter = async_graphql_warp::graphql(schema).and_then(|(schema, request): (MySchema, async_graphql::Request)| async move {
    // Execute query
    let resp = schema.execute(request).await;

    // Return result
    Ok::<_, Infallible>(async_graphql_warp::GraphQLResponse::from(resp))
});
warp::serve(filter).run(([0, 0, 0, 0], 8000)).await;
}
}

Subscription example


#![allow(unused)]
fn main() {
extern crate async_graphql_warp;
extern crate async_graphql;
extern crate warp;
use async_graphql::*;
use futures_util::stream::{Stream, StreamExt};
use std::convert::Infallible;
use warp::Filter;
struct SubscriptionRoot;
#[Subscription]
impl SubscriptionRoot {
  async fn tick(&self) -> impl Stream<Item = i32> {
    futures_util::stream::iter(0..10)
  }
}
struct QueryRoot;
#[Object]
impl QueryRoot { async fn version(&self) -> &str { "1.0" } }
async fn other() {
let schema = Schema::new(QueryRoot, EmptyMutation, SubscriptionRoot);
let filter = async_graphql_warp::graphql_subscription(schema);
warp::serve(filter).run(([0, 0, 0, 0], 8000)).await;
}
}

More examples

https://github.com/async-graphql/examples/tree/master/warp

Actix-web

Request example

When you define your actix_web::App you need to pass in the Schema as data.


#![allow(unused)]
fn main() {
extern crate async_graphql_actix_web;
extern crate async_graphql;
extern crate actix_web;
use async_graphql::*;
#[derive(Default,SimpleObject)]
struct Query { a: i32 }
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription).finish();
use actix_web::{web, HttpRequest, HttpResponse};
use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse};
async fn index(
    // Schema now accessible here
    schema: web::Data<Schema<Query, EmptyMutation, EmptySubscription>>,
    request: GraphQLRequest,
) -> web::Json<GraphQLResponse> {
    web::Json(schema.execute(request.into_inner()).await.into())
}
}

Subscription example


#![allow(unused)]
fn main() {
extern crate async_graphql_actix_web;
extern crate async_graphql;
extern crate actix_web;
use async_graphql::*;
#[derive(Default,SimpleObject)]
struct Query { a: i32 }
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription).finish();
use actix_web::{web, HttpRequest, HttpResponse};
use async_graphql_actix_web::GraphQLSubscription;
async fn index_ws(
    schema: web::Data<Schema<Query, EmptyMutation, EmptySubscription>>,
    req: HttpRequest,
    payload: web::Payload,
) -> actix_web::Result<HttpResponse> {
    GraphQLSubscription::new(Schema::clone(&*schema)).start(&req, payload)
}
}

More examples

https://github.com/async-graphql/examples/tree/master/actix-web

Advanced topics

Custom scalars

In Async-graphql most common scalar types are built in, but you can also create your own scalar types.

Using async-graphql::Scalar, you can add support for a scalar when you implement it. You only need to implement parsing and output functions.

The following example defines a 64-bit integer scalar where its input and output are strings.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct StringNumber(i64);

#[Scalar]
impl ScalarType for StringNumber {
    fn parse(value: Value) -> InputValueResult<Self> {
        if let Value::String(value) = &value {
            // Parse the integer value
            Ok(value.parse().map(StringNumber)?)
        } else {
            // If the type does not match
            Err(InputValueError::expected_type(value))
        }
    }

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

Use scalar! macro to define scalar

If your type implemented serde::Serialize and serde::Deserialize, then you can use this macro to define a scalar more simply.


#![allow(unused)]
fn main() {
extern crate async_graphql;
extern crate serde;
use async_graphql::*;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
struct MyValue {
    a: i32,
    b: HashMap<String, i32>,     
}

scalar!(MyValue);

// Rename to `MV`.
// scalar!(MyValue, "MV");

// Rename to `MV` and add description.
// scalar!(MyValue, "MV", "This is my value");
}

Optimizing N+1 queries

Have you noticed some GraphQL queries end can make hundreds of database queries, often with mostly repeated data? Lets take a look why and how to fix it.

Query Resolution

Imagine if you have a simple query like this:

query { todos { users { name } } }

and User resolver is like this:

struct User {
    id: u64,
}

#[Object]
impl User {
    async fn name(&self, ctx: &Context<'_>) -> Result<String> {
        let pool = ctx.data_unchecked::<Pool<Postgres>>();
        let (name,): (String,) = sqlx::query_as("SELECT name FROM user WHERE id = $1")
            .bind(self.id)
            .fetch_one(pool)
            .await?;
        Ok(name)
    }
}

The query executor will call the Todos resolver which does a select * from todo and return N todos. Then for each of the todos, concurrently, call the User resolver, SELECT from USER where id = todo.user_id.

eg:

SELECT id, todo, user_id FROM todo
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1

After executing SELECT name FROM user WHERE id = $1 many times, and most Todo objects belong to the same user, we need to optimize these codes!

Dataloader

We need to group queries and exclude duplicate queries. Dataloader can do this. facebook gives a request-scope batch and caching solution.

The following is an example of using DataLoader to optimize queries::

use async_graphql::*;
use async_graphql::dataloader::*;
use itertools::Itertools;
use std::sync::Arc;

struct UserNameLoader {
    pool: sqlx::Pool<Postgres>,
}

#[async_trait::async_trait]
impl Loader<u64> for UserNameLoader {
    type Value = String;
    type Error = Arc<sqlx::Error>;

    async fn load(&self, keys: &[u64]) -> Result<HashMap<u64, Self::Value>, Self::Error> {
        let query = format!("SELECT name FROM user WHERE id IN ({})", keys.iter().join(","));
        Ok(sqlx::query_as(query)
            .fetch(&self.pool)
            .map_ok(|name: String| name)
            .map_err(Arc::new)
            .try_collect().await?)
    }
}

struct User {
    id: u64,
}

#[Object]
impl User {
    async fn name(&self, ctx: &Context<'_>) -> Result<String> {
        let loader = ctx.data_unchecked::<DataLoader<UserNameLoader>>();
        let name: Option<String> = loader.load_one(self.id).await?;
        name.ok_or_else(|| "Not found".into())
    }
}

In the end, only two SQLs are needed to query the results we want!

SELECT id, todo, user_id FROM todo
SELECT name FROM user WHERE id IN (1, 2, 3, 4)

Implement multiple data types

You can implement multiple data types for the same Loader, like this:

extern crate async_graphql;
use async_graphql::*;
struct PostgresLoader {
    pool: sqlx::Pool<Postgres>,
}

#[async_trait::async_trait]
impl Loader<UserId> for PostgresLoader {
    type Value = User;
    type Error = Arc<sqlx::Error>;

    async fn load(&self, keys: &[UserId]) -> Result<HashMap<UserId, Self::Value>, Self::Error> {
        // Load users from database
    }
}

#[async_trait::async_trait]
impl Loader<TodoId> for PostgresLoader {
    type Value = Todo;
    type Error = sqlx::Error;

    async fn load(&self, keys: &[TodoId]) -> Result<HashMap<TodoId, Self::Value>, Self::Error> {
        // Load todos from database
    }
}

Custom directive

Async-graphql can easily customize directives, which can extend the behavior of GraphQL.

To create a custom directive, you need to implement the CustomDirective trait, and then use the Directive macro to generate a factory function that receives the parameters of the directive and returns an instance of the directive.

Currently Async-graphql only supports directive located at FIELD.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct ConcatDirective {
    value: String,
}

#[async_trait::async_trait]
impl CustomDirective for ConcatDirective {
    async fn resolve_field(&self, _ctx: &Context<'_>, resolve: ResolveFut<'_>) -> ServerResult<Option<Value>> {
        resolve.await.map(|value| {
            value.map(|value| match value {
                Value::String(str) => Value::String(str + &self.value),
                _ => value,
            })
        })
    }
}

#[Directive(location = "field")]
fn concat(value: String) -> impl CustomDirective {
    ConcatDirective { value }
}
}

Register the directive when building the schema:


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query { async fn version(&self) -> &str { "1.0" } }
struct ConcatDirective { value: String, }
#[async_trait::async_trait]
impl CustomDirective for ConcatDirective {
  async fn resolve_field(&self, _ctx: &Context<'_>, resolve: ResolveFut<'_>) -> ServerResult<Option<Value>> { todo!() }
}
#[Directive(location = "field")]
fn concat(value: String) -> impl CustomDirective { ConcatDirective { value } }
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
    .directive(concat)
    .finish();
}

Apollo Federation

Apollo Federation is a GraphQL API gateway which can combine multiple GraphQL services, allowing each service to implement the subset of the API it is responsible for. You can read more in the official documentation.

Async-graphql supports all the functionality of Apollo Federation, but some modifications to your Schema are required.

  • You can use the extends property declaration on async_graphql::Object and async_graphql::Interface to extend a type offered by another implementing service.

  • The external property declares that a field comes from another service。

  • The provides directive is used to annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway.

  • The requires directive is used to annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services.

Entity lookup function


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct User { id: ID }
struct Query;

#[Object]
impl Query {
    #[graphql(entity)]
    async fn find_user_by_id(&self, id: ID) -> User {
        User { id }
    }

    #[graphql(entity)]
    async fn find_user_by_id_with_username(&self, #[graphql(key)] id: ID, username: String) -> User {
        User { id }
    }

    #[graphql(entity)]
    async fn find_user_by_id_and_username(&self, id: ID, username: String) -> User {
        User { id }
    }
}
}

Notice the difference between these three lookup functions, which are all looking for the User object.

  • find_user_by_id

    Use id to find an User object, the key for User is id.

  • find_user_by_id_with_username

    Use id to find an User object, the key for User is id, and the username field value of the User object is requested.

  • find_user_by_id_and_username

    Use id and username to find an User object, the keys for User are id and username.

For a complete example, refer to: https://github.com/async-graphql/examples/tree/master/federation.

Defining a compound primary key

A single primary key can consist of multiple fields, and even nested fields, you can use InputObject to implements a nested primary key.

In the following example, the primary key of the User object is key { a b }.


#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct User { id: i32 }
#[derive(InputObject)]
struct NestedKey {
  a: i32,
  b: i32,
}

struct Query;

#[Object]
impl Query {
  #[graphql(entity)]
  async fn find_user_by_key(&self, key: NestedKey) -> User {
    User { id: key.a }
  }
}
}