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 = "2.0"
async-graphql-actix-web = "2.0" # If you need to integrate into actix-web
async-graphql-warp = "2.0" # If you need to integrate into warp
async-graphql-tide = "2.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() {
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() {
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let res = schema.execute("{ add(a: 10, b: 20) }").await;
}

Output the query results as JSON


#![allow(unused)]
fn main() {
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() {
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 conjuction 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() {
#[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() {
#[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() {
#[derive(SimpleObject)]
pub struct YetAnotherObject {
    a: SomeGenericObject<SomeType>,
    b: SomeGenericObject<SomeOtherType>,
}
}

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

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 resolvers 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 to 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() {
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 environnement 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() {
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 usefull 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() {
  let schema = Schema::build(Query::default(), Mutation::default(), 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() {
let graphql_post = warp::post()
  .and(warp::path("graphql"))
  .and(schema_filter)
  .and(a_warp_filter)
  ...
  .and_then( |schema: (Schema<Query, Mutation, Subscriptions>, async_graphql::Request), arg2: ArgType2 ...| async move {
    let (schema, request) = schema;
    let your_auth_data = auth_function_from_headers(headers).await?;
    let response = schema
      .execute(
        request
         .data(your_auth_data)
         .data(something_else)
      ).await;

    Ok(async_graphql_warp::Response::from(response))
  });
}

Headers

With the Context you can also insert and appends headers.


#![allow(unused)]
fn main() {
#[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 accross the query with ctx.fields() which will give you a SelectionField which will allow you to navigate accross the fields and subfields.

If you want to perform a search accross 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() {
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:


#![allow(unused)]
fn main() {
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() {
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(|_| json!({"code": 400})))?)
    }
}
}

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.


#![allow(unused)]
fn main() {
#[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() {
#[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() {
#[derive(Default)]
struct Subscription1;

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

#[derive(Default)]
struct Subscription2;

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

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

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

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() {
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() {
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() {
#[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() {
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() {
#[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() {
Schema::build(Query, EmptyMutation, EmptySubscription)
    .register_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() {
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() {
#[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 {
    // ...
}

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

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

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

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


#![allow(unused)]
fn main() {
#[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() {
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.
        // ...
    }
}
}

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() {
#[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() {
#[derive(InputObject)]
pub struct YetAnotherInput {
    a: SomeGenericInput<SomeType>,
    b: SomeGenericInput<SomeOtherType>,
}
}

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

Default value

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

Object field


#![allow(unused)]
fn main() {
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()
    fn test1(&self, #[graphql(default)] value: i32) {}

    // The default value of the value parameter is 10
    fn test2(&self, #[graphql(default = 10)] value: i32) {}

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

Interface field


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

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

Input object field


#![allow(unused)]
fn main() {
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() {
use async_graphql::*;

struct Query;

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

}

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() {
use async_graphql::*;

struct Mutation;

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

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

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() {
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.

use async_graphql::*;

struct Query;

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

#[tokio::main]
async fn main() {
    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 to a field of an Object. This permits you to add checks before runing the code logic of the field. Guard are made of rules that you need to define before. A rule is a structure which implements the trait Guard.


#![allow(unused)]
fn main() {
#[derive(Eq, PartialEq, Copy, Clone)]
enum Role {
    Admin,
    Guest,
}

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

Once you have defined your rule you can use it in the guard field attribute. This attribute supports 4 operators to create complex rules :

  • and : perform an and operation between two rules. (If one rule returns an error the and operator will return the error. If both rules return an error it's the first one that will be returned).

  • or : performs an or operation between two rules. (If both rules return an error the error returned is the first one)

  • chain : takes a set of rules and runs them until one returns an error or it returns Ok if all rules pass.

  • race : takes a set of rules and runs them until one returns Ok if they all fail, it returns the last error.


#![allow(unused)]
fn main() {
#[derive(SimpleObject)]
struct Query {
    #[graphql(guard(RoleGuard(role = "Role::Admin")))]
    value: i32,
    #[graphql(guard(and(
        RoleGuard(role = "Role::Admin"),
        UserGuard(username = r#""test""#)
    )))]
    value2: i32,
    #[graphql(guard(or(
        RoleGuard(role = "Role::Admin"),
        UserGuard(username = r#""test""#)
    )))]
    value3: i32,
    #[graphql(guard(chain(
        RoleGuard(role = "Role::Admin"),
        UserGuard(username = r#""test""#),
        AgeGuard(age = r#"21"#)
    )))]
    value4: i32,
    #[graphql(guard(race(
        RoleGuard(role = "Role::Admin"),
        UserGuard(username = r#""test""#),
        AgeGuard(age = r#"21"#)
    )))]
    value5: i32,
}
}

Input value validators

Arguments to a query (InputObject) are called Input Objects in GraphQL. If the provided input type does not match for a query, the query will return a type mismatch error. But, sometimes we want to provide more restrictions on specific types of values. For example, we might want to require that an argument is a valid email address. Async-graphql provides an input validators to solve this problem.

An input validator can be combined via and and or operators.

The following is an input validator which checks that a String is a valid Email or MAC address:


#![allow(unused)]
fn main() {
use async_graphql::*;
use async_graphql::validators::{Email, MAC};

struct Query;

#[Object]
impl Query {
    async fn input(#[graphql(validator(or(Email, MAC(colon = "false"))))] a: String) {
    }
}
}

The following example verifies that the i32 parameter a is greater than 10 and less than 100, or else equal to 0:


#![allow(unused)]
fn main() {
use async_graphql::*;
use async_graphql::validators::{IntGreaterThan, IntLessThan, IntEqual};

struct Query;

#[Object]
impl Query {
    async fn input(#[graphql(validator(
        or(
            and(IntGreaterThan(value = "10"), IntLessThan(value = "100")),
            IntEqual(value = "0")
        )))] a: String) {
    }
}
}

Validate the elements of the list.

You can use the list operator to indicate that the internal validator is used for all elements in a list:


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

struct Query;

#[Object]
impl Query {
    async fn input(#[graphql(validator(list(Email)))] emails: Vec<String>) {
    }
}
}

Custom validator

Here is an example of a custom validator:


#![allow(unused)]
fn main() {
struct MustBeZero {}

impl InputValueValidator for MustBeZero {
    fn is_valid(&self, value: &Value) -> Result<(), String> {
        if let Value::Int(n) = value {
            if n.as_i64().unwrap() != 0 {
                // Validation failed
                Err(format!(
                    "the value is {}, but must be zero",
                    n.as_i64().unwrap(),
                ))
            } else {
                // Validation succeeded
                Ok(())
            }
        } else {
            // If the type does not match we can return None and built-in validations
            // will pick up on the error
            Ok(())
        }
    }
}
}

Here is an example of a custom validator with extensions (return async_graphql::Error):


#![allow(unused)]
fn main() {
pub struct Email;

impl InputValueValidator for Email {
    fn is_valid_with_extensions(&self, value: &Value) -> Result<(), Error> {
        if let Value::String(s) = value {
            if &s.to_lowercase() != s {
                return Err(Error::new("Validation Error").extend_with(|_, e| {
                    e.set("key", "email_must_lowercase")
                }));
            }

            if !validate_non_control_character(s) {
                return Err(Error::new("Validation Error").extend_with(|_, e| {
                    e.set("key", "email_must_no_non_control_character")
                }));
            }

            if !validate_email(s) {
                return Err(Error::new("Validation Error").extend_with(|_, e| {
                    e.set("key", "invalid_email_format")
                }));
            }
        }

        Ok(())
    }
}
}

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() {
#[Object(cache_control(max_age = 60))]
impl Query {
    #[graphql(cache_control(max_age = 30))]
    async fn value1(&self) -> i32 {
    }

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

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

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() {
use async_graphql::*;
use async_graphql::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.append(
                (start..end).into_iter().map(|n|
                    Ok(Edge::new_with_additional_fields(n, n as i32, EmptyFields)),
            ))?;
            Ok(connection)
        })
    }
}

}

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() {
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() {
use std::num::ParseIntError;
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() {
#[macro_use]
extern crate thiserror;

#[derive(Debug, 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),
              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() {
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.


#![allow(unused)]
fn main() {
use async_graphql::*;
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() {
use async_graphql::*;
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() {
use async_graphql::*;
use async_graphql::extensions::ApolloTracing;

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() {
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() {
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() {
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 in the execution phase, so you don't have to worry about the query over-limit causing the execute only part of the query.

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() {
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)]
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
}

}

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.

Tide

async_graphql_tide provides an implementation of tide::Endpoint trait. It also provides receive_request and respond functions to convert a Tide request to a GraphQL request and back to Tide response, if you want to handle the request manually.

Request example

When you create your tide server, you will need to pass the async_graphql_tide::endpoint with your schema as the POST request handler. Please note that you need to enable the attributes feature in async-std for this example to work.

use async_graphql::{
    http::{playground_source, GraphQLPlaygroundConfig},
    Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject,
};
use tide::{http::mime, Body, Response, StatusCode};

#[derive(SimpleObject)]
pub struct Demo {
    pub id: usize,
}

pub struct QueryRoot;

#[Object]
impl QueryRoot {
    async fn demo(&self, _ctx: &Context<'_>) -> Demo {
        Demo { id: 42 }
    }
}

#[async_std::main]
async fn main() -> tide::Result<()> {
    let mut app = tide::new();

    // create schema
    let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).finish();

    // add tide endpoint
    app.at("/graphql")
        .post(async_graphql_tide::endpoint(schema));

    // enable graphql playground
    app.at("/").get(|_| async move {
        Ok(Response::builder(StatusCode::Ok)
            .body(Body::from_string(playground_source(
                // note that the playground needs to know
                // the path to the graphql endpoint
                GraphQLPlaygroundConfig::new("/graphql"),
            )))
            .content_type(mime::HTML)
            .build())
    });

    Ok(app.listen("127.0.0.1:8080").await?)
}

Manually handle the request

If you want to manually handle the request, for example to read a header, you can skip async_graphql_tide::endpoint and use receive_request and respond functions instead.


#![allow(unused)]
fn main() {
app.at("/graphql").post(move |req: tide::Request<()>| {
    let schema = schema.clone();
    async move {
        let req = async_graphql_tide::receive_request(req).await?;
        async_graphql_tide::respond(schema.execute(req).await)
    }
});
}

More examples

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

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 always asks for the POST method and outputs a 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() {
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::Response::from(resp))
});
warp::serve(filter).run(([0, 0, 0, 0], 8000)).await;
}

Subscription example


#![allow(unused)]
fn main() {
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

Async-graphql-actix-web provides an implementation of actix_web::FromRequest for Request. This is actually an abstraction around async_graphql::Request and you can call Request::into_inner to convert it into a async_graphql::Request.

WSSubscription is an Actor that supports WebSocket subscriptions.

Request example

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


#![allow(unused)]
fn main() {
async fn index(
    // Schema now accessible here
    schema: web::Data<Schema>,
    request: async_graphql_actix_web::Request,
) -> web::Json<Response> {
    web::Json(Response(schema.execute(request.into_inner()).await)
}

}

Subscription example


#![allow(unused)]
fn main() {
async fn index_ws(
    schema: web::Data<Schema>,
    req: HttpRequest,
    payload: web::Payload,
) -> Result<HttpResponse> {
    WSSubscription::start(Schema::clone(&*schema), &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. (Note: Async-graphql already supports 64-bit integers and uses strings as input and output.)


#![allow(unused)]
fn main() {
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() {
#[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:


#![allow(unused)]
fn main() {
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::


#![allow(unused)]
fn main() {
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 pool = ctx.data_unchecked::<Pool<Postgres>>();
        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:


#![allow(unused)]
fn main() {
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 extensions

A GraphQL extension object can receive events in various stages of a query's execution, and you can collect various kinds of data to be returned in the query results.

You can use async_graphql::Extension to define an extension object, and your application must call Schema::extension when your Schema is created.

You can refer to Apollo Tracing to implement your own extension types.

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() {
struct Query;

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

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

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

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() {
#[derive(InputObject)]
struct NestedKey {
  a: i32,
  b: i32,
}

struct Query;

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