Introduction

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

Juniper is a library for creating GraphQL servers in Rust. Build type-safe and fast API servers with minimal boilerplate and configuration (we do try to make declaring and resolving GraphQL schemas as convenient as Rust will allow).

Juniper doesn't include a web server itself, instead, it provides building blocks to make integration with existing web servers straightforward. It optionally provides a pre-built integration for some widely used web server frameworks in Rust ecosystem.

Features

Juniper supports the full GraphQL query language according to the specification (October 2021).

NOTE: As an exception to other GraphQL libraries for other languages, Juniper builds non-null types by default. A field of type Vec<Episode> will be converted into [Episode!]!. The corresponding Rust type for a nullable [Episode] would be Option<Vec<Option<Episode>>> instead.

Integrations

Types

Juniper provides out-of-the-box integration for some very common Rust crates to make building schemas a breeze. The types from these crates will be usable in your schemas automatically after enabling the correspondent self-titled Cargo feature:

Web server frameworks

API stability

Juniper has not reached 1.0 yet, thus some API instability should be expected.

Quickstart

This page will give you a short introduction to the concepts in Juniper.

Juniper follows a code-first approach to define a GraphQL schema.

TIP: For a schema-first approach, consider using a juniper-from-schema crate for generating a juniper-based code from a schema file.

Installation

[dependencies]
juniper = "0.16.1"

Schema

Exposing simple enums and structs as GraphQL types is just a matter of adding a custom derive attribute to them. Juniper includes support for basic Rust types that naturally map to GraphQL features, such as Option<T>, Vec<T>, Box<T>, Arc<T>, String, f64, i32, references, slices and arrays.

For more advanced mappings, Juniper provides multiple macros to map your Rust types to a GraphQL schema. The most important one is the #[graphql_object] attribute that is used for declaring a GraphQL object with resolvers (typically used for declaring Query and Mutation roots).

# ![allow(unused_variables)]
extern crate juniper;

use std::fmt::Display;

use juniper::{
    graphql_object, EmptySubscription, FieldResult, GraphQLEnum,
    GraphQLInputObject, GraphQLObject, ScalarValue,
};

struct DatabasePool;
impl DatabasePool {
    fn get_connection(&self) -> FieldResult<DatabasePool> { Ok(DatabasePool) }
    fn find_human(&self, _id: &str) -> FieldResult<Human> { Err("")? }
    fn insert_human(&self, _human: &NewHuman) -> FieldResult<Human> { Err("")? }
}

#[derive(GraphQLEnum)]
enum Episode {
    NewHope,
    Empire,
    Jedi,
}

#[derive(GraphQLObject)]
#[graphql(description = "A humanoid creature in the Star Wars universe")]
struct Human {
    id: String,
    name: String,
    appears_in: Vec<Episode>,
    home_planet: String,
}

// There is also a custom derive for mapping GraphQL input objects.
#[derive(GraphQLInputObject)]
#[graphql(description = "A humanoid creature in the Star Wars universe")]
struct NewHuman {
    name: String,
    appears_in: Vec<Episode>,
    home_planet: String,
}

// Now, we create our root `Query` and `Mutation` types with resolvers by using 
// the `#[graphql_object]` attribute.

// Resolvers can have a context that allows accessing shared state like a 
// database pool.
struct Context {
    // Use your real database pool here.
    db: DatabasePool,
}

// To make our `Context` usable by `juniper`, we have to implement a marker 
// trait.
impl juniper::Context for Context {}

struct Query;

// Here we specify the context type for the object.
// We need to do this in every type that needs access to the `Context`.
#[graphql_object]
#[graphql(context = Context)]
impl Query {
    // Note, that the field name will be automatically converted to the
    // `camelCased` variant, just as GraphQL conventions imply.
    fn api_version() -> &'static str {
        "1.0"
    }

    fn human(
        // Arguments to resolvers can either be simple scalar types, enums or 
        // input objects.
        id: String,
        // To gain access to the `Context`, we specify a `context`-named 
        // argument referring the correspondent `Context` type, and `juniper`
        // will inject it automatically.
        context: &Context,
    ) -> FieldResult<Human> {
        // Get a `db` connection.
        let conn = context.db.get_connection()?;
        // Execute a `db` query.
        // Note the use of `?` to propagate errors.
        let human = conn.find_human(&id)?;
        // Return the result.
        Ok(human)
    }
}

// Now, we do the same for our `Mutation` type.

struct Mutation;

#[graphql_object]
#[graphql(
    context = Context,
    // If we need to use `ScalarValue` parametrization explicitly somewhere
    // in the object definition (like here in `FieldResult`), we could
    // declare an explicit type parameter for that, and specify it.
    scalar = S: ScalarValue + Display,
)]
impl Mutation {
    fn create_human<S: ScalarValue + Display>(
        new_human: NewHuman,
        context: &Context,
    ) -> FieldResult<Human, S> {
        let db = context.db.get_connection().map_err(|e| e.map_scalar_value())?;
        let human: Human = db.insert_human(&new_human).map_err(|e| e.map_scalar_value())?;
        Ok(human)
    }
}

// Root schema consists of a query, a mutation, and a subscription.
// Request queries can be executed against a `RootNode`.
type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription<Context>>;

fn main() {
    _ = Schema::new(Query, Mutation, EmptySubscription::new());
}

Now we have a very simple but functional schema for a GraphQL server!

To actually serve the schema, see the guides for our various server integrations.

Execution

Juniper is a library that can be used in many contexts: it doesn't require a server, nor it has a dependency on a particular transport or serialization format. You can invoke the juniper::execute() directly to get a result for a GraphQL query:

// Only needed due to 2018 edition because the macro is not accessible.
#[macro_use] extern crate juniper;
use juniper::{
    graphql_object, graphql_value, EmptyMutation, EmptySubscription, 
    GraphQLEnum, Variables, 
};

#[derive(GraphQLEnum, Clone, Copy)]
enum Episode {
    // Note, that the enum value will be automatically converted to the
    // `SCREAMING_SNAKE_CASE` variant, just as GraphQL conventions imply.
    NewHope,
    Empire,
    Jedi,
}

// Arbitrary context data.
struct Ctx(Episode);

impl juniper::Context for Ctx {}

struct Query;

#[graphql_object]
#[graphql(context = Ctx)]
impl Query {
    fn favorite_episode(context: &Ctx) -> Episode {
        context.0
    }
}

type Schema = juniper::RootNode<'static, Query, EmptyMutation<Ctx>, EmptySubscription<Ctx>>;

fn main() {
    // Create a context.
    let ctx = Ctx(Episode::NewHope);

    // Run the execution.
    let (res, _errors) = juniper::execute_sync(
        "query { favoriteEpisode }",
        None,
        &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
        &Variables::new(),
        &ctx,
    ).unwrap();

    assert_eq!(
        res,
        graphql_value!({
            "favoriteEpisode": "NEW_HOPE",
        }),
    );
}

Type system

Most of the work in working with Juniper consists of mapping the GraphQL type system to the Rust types our application uses.

Juniper provides some convenient abstractions making this process as painless as possible.

Find out more in the individual chapters below:

Objects

GraphQL objects represent a list of named fields, each of which yield a value of a specific type.

When declaring a GraphQL schema, most of the time we deal with GraphQL objects, because they are the only place where we actually define the behavior once schema gets executed.

There are two ways to define a GraphQL object in Juniper:

  1. The easiest way, suitable for trivial cases, is to use the #[derive(GraphQLObject)] attribute on a struct, as described below.
  2. The other way, using the #[graphql_object] attribute, is described in the "Complex fields" chapter.

Trivial

While any type in Rust can be exposed as a GraphQL object, the most common one is a struct:

extern crate juniper;
use juniper::GraphQLObject;

#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
}

fn main() {}

This creates a GraphQL object type called Person, with two fields: name of type String!, and age of type Int!. Because of Rust's type system, everything is exported as non-null by default.

TIP: If a nullable field is required, the most obvious way is to use Option. Or Nullable for distinguishing between explicit and implicit nulls.

Documentation

We should take advantage of the fact that GraphQL is self-documenting and add descriptions to the defined GraphQL object type and its fields. Juniper will automatically use associated Rust doc comments as GraphQL descriptions:

extern crate juniper;
use juniper::GraphQLObject;

/// Information about a person.
#[derive(GraphQLObject)]
struct Person {
    /// The person's full name, including both first and last names.
    name: String,

    /// The person's age in years, rounded down.
    age: i32,
}

fn main() {}

If using Rust doc comments is not desired (for example, when we want to keep Rust API docs and GraphQL schema descriptions different), the #[graphql(description = "...")] attribute can be used instead, which takes precedence over Rust doc comments:

extern crate juniper;
use juniper::GraphQLObject;

/// This doc comment is visible only in Rust API docs.
#[derive(GraphQLObject)]
#[graphql(description = "This description is visible only in GraphQL schema.")]
struct Person {
    /// This doc comment is visible only in Rust API docs.
    #[graphql(desc = "This description is visible only in GraphQL schema.")]
    //        ^^^^ shortcut for a `description` argument
    name: String,

    /// This doc comment is visible in both Rust API docs and GraphQL schema 
    /// descriptions.
    age: i32,
}

fn main() {}

Renaming

By default, struct fields are converted from Rust's standard snake_case naming convention into GraphQL's camelCase convention:

extern crate juniper;
use juniper::GraphQLObject;

#[derive(GraphQLObject)]
struct Person {
    first_name: String, // exposed as `firstName` in GraphQL schema
    last_name: String,  // exposed as `lastName` in GraphQL schema
}

fn main() {}

We can override the name by using the #[graphql(name = "...")] attribute:

extern crate juniper;
use juniper::GraphQLObject;

#[derive(GraphQLObject)]
#[graphql(name = "WebPerson")] // now exposed as `WebPerson` in GraphQL schema
struct Person {
    name: String,
    age: i32,
    #[graphql(name = "websiteURL")]
    website_url: Option<String>, // now exposed as `websiteURL` in GraphQL schema
}

fn main() {}

Or provide a different renaming policy for all the struct fields:

extern crate juniper;
use juniper::GraphQLObject;

#[derive(GraphQLObject)]
#[graphql(rename_all = "none")] // disables any renaming
struct Person {
    name: String,
    age: i32,
    website_url: Option<String>, // exposed as `website_url` in GraphQL schema
}

fn main() {}

TIP: Supported policies are: SCREAMING_SNAKE_CASE, camelCase and none (disables any renaming).

Deprecation

To deprecate a GraphQL object field, either the #[graphql(deprecated = "...")] attribute, or Rust's #[deprecated] attribute, should be used:

extern crate juniper;
use juniper::GraphQLObject;

#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
    #[graphql(deprecated = "Please use the `name` field instead.")]
    first_name: String,
    #[deprecated(note = "Please use the `name` field instead.")]
    last_name: String,
}

fn main() {}

NOTE: Only GraphQL object/interface fields and GraphQL enum values can be deprecated.

Ignoring

By default, all struct fields are included into the generated GraphQL object type. To prevent inclusion of a specific field annotate it with the #[graphql(ignore)] attribute:

#![allow(dead_code)]
extern crate juniper;
use juniper::GraphQLObject;

#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
    #[graphql(ignore)]
    password_hash: String, // cannot be queried from GraphQL
    #[graphql(skip)]
    //        ^^^^ alternative naming, up to your preference
    is_banned: bool,       // cannot be queried from GraphQL
}

fn main() {}

TIP: See more available features in the API docs of the #[derive(GraphQLObject)] attribute.

Relationships

GraphQL object fields can be of any GraphQL type, except input objects.

Let's see what it means to build relationships between objects:

extern crate juniper;
use juniper::GraphQLObject;

#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
}

#[derive(GraphQLObject)]
struct House {
    address: Option<String>,  // converted into `String` (`null`able)
    inhabitants: Vec<Person>, // converted into `[Person!]!`
}

fn main() {}

Because Person is a valid GraphQL type, we can have a Vec<Person> in a struct, and it'll be automatically converted into a list of non-nullable Person objects.

Complex fields

Using a plain Rust struct for representing a GraphQL object is easy and trivial but does not cover every case. What if we need to express something non-trivial as a GraphQL field, such as:

To support these more complicated use cases, we need a way to define a GraphQL field as a function. In Juniper this is achievable by placing the #[graphql_object] attribute on an impl block, which turns its methods into GraphQL fields:

extern crate juniper;
use juniper::{graphql_object, GraphQLObject};

#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
}

struct House {
    inhabitants: Vec<Person>,
}

// Defines the `House` GraphQL object.
#[graphql_object]
impl House {
    // Creates the field `inhabitantWithName(name: String!)`, 
    // returning a `null`able `Person`.
    fn inhabitant_with_name(&self, name: String) -> Option<&Person> {
        self.inhabitants.iter().find(|p| p.name == name)
    }
}

fn main() {}

NOTE: To access global data such as database connections or authentication information, a context is used. To learn more about this, see the "Context" chapter.

Default arguments

Though Rust doesn't have the notion of default arguments, GraphQL arguments are able to have default values. These default values are used when a GraphQL operation doesn't specify the argument explicitly. In Juniper, defining a default value for a GraphQL argument is enabled by the #[graphql(default)] attribute:

extern crate juniper;
use juniper::graphql_object;

struct Person;

#[graphql_object]
impl Person {
    fn field1(
        // Default value can be any valid Rust expression, including a function
        // call, etc.
        #[graphql(default = true)]
        arg1: bool,
        // If default expression is not specified, then the `Default::default()` 
        // value is used.
        #[graphql(default)]
        arg2: i32,
    ) -> String {
        format!("{arg1} {arg2}")
    }
}

fn main() {}

Renaming

Like with the #[derive(GraphQLObject)] attribute on structs, field names are converted from Rust's standard snake_case naming convention into GraphQL's camelCase convention.

We can override the name by using the #[graphql(name = "...")] attribute:

extern crate juniper;
use juniper::graphql_object;

struct Person;

#[graphql_object]
#[graphql(name = "PersonObject")]
impl Person { // exposed as `PersonObject` in GraphQL schema
    #[graphql(name = "myCustomFieldName")]
    fn renamed_field( // exposed as `myCustomFieldName` in GraphQL schema
        #[graphql(name = "myArgument")]
        renamed_argument: bool, // exposed as `myArgument` in GraphQL schema
    ) -> bool {
        renamed_argument
    }
}

fn main() {}

Or provide a different renaming policy for all the defined fields:

extern crate juniper;
use juniper::graphql_object;

struct Person;

#[graphql_object]
#[graphql(rename_all = "none")] // disables any renaming
impl Person {
    fn renamed_field( // exposed as `renamed_field` in GraphQL schema
        renamed_argument: bool, // exposed as `renamed_argument` in GraphQL schema
    ) -> bool {
        renamed_argument
    }
}

fn main() {}

TIP: Supported policies are: SCREAMING_SNAKE_CASE, camelCase and none (disables any renaming).

Documentation and deprecation

Similarly, GraphQL fields may also be documented and deprecated via #[graphql(description = "...")] and #[graphql(deprecated = "...")]/#[deprecated] attributes:

extern crate juniper;
use juniper::graphql_object;

struct Person;

/// This doc comment is visible only in Rust API docs.
#[graphql_object]
#[graphql(description = "This description overwrites the one from doc comment.")]
impl Person {
    /// This doc comment is visible only in Rust API docs.
    #[graphql(description = "This description is visible only in GraphQL schema.")]
    fn empty() -> &'static str {
        ""
    }
    
    #[graphql(desc = "This description is visible only in GraphQL schema.")]
    //        ^^^^ shortcut for a `description` argument
    fn field(
        #[graphql(desc = "This description is visible only in GraphQL schema.")]
        arg: bool,
    ) -> bool {
        arg
    }

    /// This doc comment is visible in both Rust API docs and GraphQL schema 
    /// descriptions.
    #[graphql(deprecated = "Just because.")]
    fn deprecated_graphql() -> bool {
        true
    }
    
    // Standard Rust's `#[deprecated]` attribute works too!
    #[deprecated(note = "Reason is optional, btw!")]
    fn deprecated_standard() -> bool { // has no description in GraphQL schema
        false
    }
}

fn main() {}

NOTE: Only GraphQL object/interface fields and GraphQL enum values can be deprecated.

Ignoring

By default, all methods of an impl block are exposed as GraphQL fields. If a method should not be exposed as a GraphQL field, it should be defined in a separate impl block or marked with the #[graphql(ignore)] attribute:

#![allow(dead_code)]
extern crate juniper;
use juniper::graphql_object;

struct Person {
    name: String,
    age: i32,
}

#[graphql_object]
impl Person {
    fn name(&self) -> &str {
        self.name.as_str()
    }

    fn age(&self) -> i32 {
        self.age
    }

    #[graphql(ignore)]
    pub fn hidden_from_graphql(&self) {
        // whatever goes...
    }

    #[graphql(skip)]
    //        ^^^^ alternative naming, up to your preference
    pub fn also_hidden_from_graphql(&self) {
        // whatever goes...
    }
}

impl Person {
    pub fn not_even_considered_for_graphql(&self) {
        // whatever goes...
    }
}

fn main() {}

TIP: See more available features in the API docs of the #[graphql_object] attribute.

Context

Context is a feature in Juniper that lets field resolvers access global data, most commonly database connections or authentication information.

Let's say that we have a simple Users database in a HashMap:

#![allow(dead_code)]
use std::collections::HashMap;

struct Database {
    users: HashMap<i32, User>,
}

struct User {
    id: i32,
    name: String,
    friend_ids: Vec<i32>,
}

fn main() {}

We would like to define a friends field on User that returns a list of User objects. In order to write such a field we need to query a Database. To accomplish this we must first mark the Database as a valid context type and then assign it to the User object. To gain access to the context in the friends field, we need to specify an argument with the same type as the specified context:

extern crate juniper;
use std::collections::HashMap;
use juniper::graphql_object;

struct Database {
    users: HashMap<i32, User>,
}

// Mark the `Database` as a valid context type for Juniper.
impl juniper::Context for Database {}

struct User {
    id: i32,
    name: String,
    friend_ids: Vec<i32>,
}

#[graphql_object]
#[graphql(context = Database)] // assign `Database` as the context type
impl User {
    // Inject the `Database` context by specifying an argument with the 
    // context type: 
    // - the type must be a reference;
    // - the name of the argument SHOULD be `context` (or `ctx`).
    fn friends<'db>(&self, context: &'db Database) -> Vec<&'db User> {
        //                 ^^^^^^^ or `ctx`, up to your preference
        self.friend_ids.iter()
            .map(|id| {
                context.users.get(&id).expect("could not find `User` with ID")
            })
            .collect()
    }

    fn friend<'db>(
        &self, 
        id: i32, 
        // Alternatively, the context argument may be marked with an attribute,
        // and thus, named arbitrary.
        #[graphql(context)] db: &'db Database,
        //        ^^^^^^^ or `ctx`, up to your preference
    ) -> Option<&'db User> {
        self.friend_ids.contains(&id).then(|| {
            db.users.get(&id).expect("could not find `User` with ID")
        })
    }

    fn name(&self) -> &str { 
        self.name.as_str() 
    }

    fn id(&self) -> i32 { 
        self.id 
    }
}

fn main() {}

Mutating and mutable references

Context cannot be a mutable reference as fields may be resolved concurrently. If something in the context requires a mutable reference, the context type should leverage the interior mutability pattern (e.g. use RwLock, RefCell or similar).

For example, when using async runtime with work stealing (like tokio), which obviously requires thread safety in addition, we will need to use a corresponding async version of RwLock:

extern crate juniper;
extern crate tokio;
use std::collections::HashMap;
use juniper::graphql_object;
use tokio::sync::RwLock;

struct Database {
    requested_count: HashMap<i32, i32>,
}

// Since we cannot directly implement `juniper::Context`
// for `RwLock`, we use the newtype idiom.
struct DatabaseContext(RwLock<Database>);

impl juniper::Context for DatabaseContext {}

struct User {
    id: i32,
    name: String
}

#[graphql_object]
#[graphql(context = DatabaseContext)]
impl User {
    async fn times_requested<'db>(&self, ctx: &'db DatabaseContext) -> i32 {
        // Acquire a mutable reference and `.await` if async `RwLock` is used,
        // which is necessary if context consists of async operations like 
        // querying remote databases.
        
        // Obtain base type.
        let DatabaseContext(db) = ctx;
        // If context is immutable use `.read()` on `RwLock` instead.
        let mut db = db.write().await;
        
        // Perform a mutable operation.
        db.requested_count
            .entry(self.id)
            .and_modify(|e| *e += 1)
            .or_insert(1)
            .clone()
    }

    fn name(&self) -> &str { 
        self.name.as_str() 
    }

    fn id(&self) -> i32 { 
        self.id 
    }
}

fn main() {}

TIP: Replace tokio::sync::RwLock with std::sync::RwLock (or similar) if you don't intend to use async resolving.

Error handling

Error handling in GraphQL can be done in multiple ways. We will cover the two different error handling models mostly used:

  1. Implicit field results.
  2. Explicit errors backend by GraphQL schema.

Choosing the right error handling method depends on the requirements of the application and the concrete error happening. Investigating both approaches is beneficial.

Comparison

The first approach (where every error is a field error) is easier to implement. However, clients won't know what errors may occur and instead will have to infer what happens from the error message. This is brittle and could change over time due to either clients or server changing. Therefore, extensive integration testing between clients and server is required to maintain the implicit contract between the two.

Encoding non-critical errors in a GraphQL schema makes the contract between clients and the server explicit. This allows clients to understand and handle these errors correctly and the server to know when changes are potentially breaking clients. However, encoding this error information into a GraphQL schema requires additional code and up-front definition of non-critical errors.

Field errors

Rust provides two ways of dealing with errors:

Juniper does not do anything about panicking, it naturally bubbles up to the surrounding code/framework and can be dealt with there.

For recoverable errors, Juniper works well with the built-in Result type. You can use the ? operator and things will work as you expect them to:

extern crate juniper;
use std::{fs::File, io::Read, path::PathBuf, str};
use juniper::{graphql_object, FieldResult};

struct Example {
    filename: PathBuf,
}

#[graphql_object]
impl Example {
    fn contents(&self) -> FieldResult<String> {
        let mut file = File::open(&self.filename)?;
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;
        Ok(contents)
    }

    fn foo() -> FieldResult<Option<String>> {
        // Some invalid bytes.
        let invalid = vec![128, 223];

        Ok(Some(str::from_utf8(&invalid)?.to_string()))
    }
}

fn main() {}

FieldResult<T> is an alias for Result<T, FieldError>, which is the error type all fallible fields must return. By using the ? operator, any type that implements the Display trait (which most of the error types out there do) can be automatically converted into a FieldError.

TIP: If a custom conversion into a FieldError is needed (to fill up extensions, for example), the IntoFieldError trait should be implemented.

NOTE: FieldErrors are GraphQL field errors and are not visible in a GraphQL schema in any way.

Error payloads, null, and partial errors

Juniper's error behavior conforms to the GraphQL specification.

When a field returns an error, the field's result is replaced by null, and an additional errors object is created at the top level of the response, and the execution is resumed.

Let's run the following query against the previous example:

{
  example {
    contents
    foo
  }
}

If str::from_utf8 results in a std::str::Utf8Error, then the following will be returned:

{
  "data": {
    "example": {
      "contents": "<Contents of the file>",
      "foo": null
    }
  },
  "errors": [{
    "message": "invalid utf-8 sequence of 2 bytes from index 0",
    "locations": [{"line": 2, "column": 4}]
  }]
}

Since Non-Null type fields cannot be null, field errors are propagated to be handled by the parent field. If the parent field may be null then it resolves to null, otherwise if it is a Non-Null type, the field error is further propagated to its parent field.

For example, with the following query:

{
  example {
    contents
  }
}

If the File::open() above results in a std::io::ErrorKind::PermissionDenied, the following ill be returned:

{
  "data": null,
  "errors": [{
    "message": "Permission denied (os error 13)",
    "locations": [{"line": 2, "column": 4}]
  }]
}

Additional information

Sometimes it's desirable to return additional structured error information to clients. This can be accomplished by implementing the IntoFieldError trait:

#[macro_use] extern crate juniper;
use juniper::{graphql_object, FieldError, IntoFieldError, ScalarValue};

enum CustomError {
    WhateverNotSet,
}

impl<S: ScalarValue> IntoFieldError<S> for CustomError {
    fn into_field_error(self) -> FieldError<S> {
        match self {
            Self::WhateverNotSet => FieldError::new(
                "Whatever does not exist",
                graphql_value!({
                    "type": "NO_WHATEVER"
                }),
            ),
        }
    }
}

struct Example {
    whatever: Option<bool>,
}

#[graphql_object]
impl Example {
    fn whatever(&self) -> Result<bool, CustomError> {
        if let Some(value) = self.whatever {
            return Ok(value);
        }
        Err(CustomError::WhateverNotSet)
    }
}

fn main() {}

And the specified structured error information will be included into the error's extensions:

{
  "errors": [{
    "message": "Whatever does not exist",
    "locations": [{"line": 2, "column": 4}],
    "extensions": {
      "type": "NO_WHATEVER"
    }
  }]
}

NOTE: This pattern is particularly useful when it comes to instrumentation of returned field errors with custom error codes or additional diagnostics (like stack traces or tracing IDs).

Schema errors

Rust's model of errors can be adapted for GraphQL. Rust's panic is similar to a field error - the whole query is aborted and nothing can be extracted (except for error related information).

Not all errors require this strict handling. Recoverable or partial errors can be put into a GraphQL schema, so the client can intelligently handle them.

To implement this approach, all errors must be partitioned into two classes:

  • Critical errors that cannot be fixed by clients (e.g. a database error).
  • Recoverable errors that can be fixed by clients (e.g. invalid input data).

Critical errors are returned from resolvers as field errors (from the previous chapter). Recoverable errors are part of a GraphQL schema and can be handled gracefully by clients. Similar to Rust, GraphQL allows similar error models with unions (see "Unions" chapter).

Example: Simple

In this example, basic input validation is implemented with GraphQL types. Strings are used to identify the problematic field name. Errors for a particular field are also returned as a string.

extern crate juniper;
use juniper::{graphql_object, GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
pub struct Item {
    name: String,
    quantity: i32,
}

#[derive(GraphQLObject)]
pub struct ValidationError {
    field: String,
    message: String,
}

#[derive(GraphQLObject)]
pub struct ValidationErrors {
    errors: Vec<ValidationError>,
}

#[derive(GraphQLUnion)]
pub enum GraphQLResult {
    Ok(Item),
    Err(ValidationErrors),
}

pub struct Mutation;

#[graphql_object]
impl Mutation {
    fn add_item(&self, name: String, quantity: i32) -> GraphQLResult {
        let mut errors = Vec::new();

        if !(10 <= name.len() && name.len() <= 100) {
            errors.push(ValidationError {
                field: "name".into(),
                message: "between 10 and 100".into(),
            });
        }

        if !(1 <= quantity && quantity <= 10) {
            errors.push(ValidationError {
                field: "quantity".into(),
                message: "between 1 and 10".into(),
            });
        }

        if errors.is_empty() {
            GraphQLResult::Ok(Item { name, quantity })
        } else {
            GraphQLResult::Err(ValidationErrors { errors })
        }
    }
}

fn main() {}

Each function may have a different return type and depending on the input parameters a new result type may be required. For example, adding a User would require a new result type containing the variant Ok(User)instead of Ok(Item).

NOTE: In this example the returned string contains a server-side localized error message. However, it is also possible to return a unique string identifier and have the client present a localized string to its users.

The client can send a mutation request and handle the resulting errors in the following manner:

{
  mutation {
    addItem(name: "", quantity: 0) {
      ... on Item {
        name
      }
      ... on ValidationErrors {
        errors {
          field
          message
        }
      }
    }
  }
}

NOTE: A useful side effect of this approach is to have partially successful queries or mutations. If one resolver fails, the results of the successful resolvers are not discarded.

Example: Complex

Instead of using strings to propagate errors, it is possible to use GraphQL type system to describe the errors more precisely.

For each fallible input argument we create a field in a GraphQL object. The field is set if the validation for that particular argument fails.

extern crate juniper;
use juniper::{graphql_object, GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
pub struct Item {
    name: String,
    quantity: i32,
}

#[derive(GraphQLObject)]
pub struct ValidationError {
    name: Option<String>,
    quantity: Option<String>,
}

#[derive(GraphQLUnion)]
pub enum GraphQLResult {
    Ok(Item),
    Err(ValidationError),
}

pub struct Mutation;

#[graphql_object]
impl Mutation {
    fn add_item(&self, name: String, quantity: i32) -> GraphQLResult {
        let mut error = ValidationError {
            name: None,
            quantity: None,
        };

        if !(10 <= name.len() && name.len() <= 100) {
            error.name = Some("between 10 and 100".into());
        }

        if !(1 <= quantity && quantity <= 10) {
            error.quantity = Some("between 1 and 10".into());
        }

        if error.name.is_none() && error.quantity.is_none() {
            GraphQLResult::Ok(Item { name, quantity })
        } else {
            GraphQLResult::Err(error)
        }
    }
}

fn main() {}

NOTE: We will likely want some kind of code generation to reduce repetition as the number of types required is significantly larger than before. Each resolver function has a custom ValidationResult which contains only fields provided by the function.

So, all the expected errors are handled directly inside the query. Additionally, all non-critical errors are known in advance by both the server and the client:

{
  mutation {
    addItem {
      ... on Item {
        name
      }
      ... on ValidationErrorsItem {
        name
        quantity
      }
    }
  }
}

Example: Complex with critical errors

Our examples so far have only included non-critical errors. Providing errors inside a GraphQL schema still allows us to return unexpected critical errors when they occur.

In the following example, a theoretical database could fail and would generate errors. Since it is not common for a database to fail, the corresponding error is returned as a critical error:

extern crate juniper;
use juniper::{graphql_object, graphql_value, FieldError, GraphQLObject, GraphQLUnion, ScalarValue};

#[derive(GraphQLObject)]
pub struct Item {
    name: String,
    quantity: i32,
}

#[derive(GraphQLObject)]
pub struct ValidationErrorItem {
    name: Option<String>,
    quantity: Option<String>,
}

#[derive(GraphQLUnion)]
pub enum GraphQLResult {
    Ok(Item),
    Err(ValidationErrorItem),
}

pub enum ApiError {
    Database,
}

impl<S: ScalarValue> juniper::IntoFieldError<S> for ApiError {
    fn into_field_error(self) -> FieldError<S> {
        match self {
            Self::Database => FieldError::new(
                "Internal database error",
                graphql_value!({"type": "DATABASE"}),
            ),
        }
    }
}

pub struct Mutation;

#[graphql_object]
impl Mutation {
    fn add_item(&self, name: String, quantity: i32) -> Result<GraphQLResult, ApiError> {
        let mut error = ValidationErrorItem {
            name: None,
            quantity: None,
        };

        if !(10 <= name.len() && name.len() <= 100) {
            error.name = Some("between 10 and 100".into());
        }

        if !(1 <= quantity && quantity <= 10) {
            error.quantity = Some("between 1 and 10".into());
        }

        if error.name.is_none() && error.quantity.is_none() {
            Ok(GraphQLResult::Ok(Item { name, quantity }))
        } else {
            Ok(GraphQLResult::Err(error))
        }
    }
}

fn main() {}

Example: Shopify API

The Shopify API implements a similar approach. Their API is a good reference to explore this approach in a real world application.

Example: Non-struct objects

Up until now, we've only looked at mapping structs to GraphQL objects. However, any Rust type can be exposed a GraphQL object.

Using Result-like enums can be a useful way of reporting validation errors from a mutation:

extern crate juniper;
use juniper::{graphql_object, GraphQLObject};

#[derive(GraphQLObject)] 
struct User { 
    name: String,
}

#[derive(GraphQLObject)]
struct ValidationError {
    field: String,
    message: String,
}

enum SignUpResult {
    Ok(User),
    Error(Vec<ValidationError>),
}

#[graphql_object]
impl SignUpResult {
    fn user(&self) -> Option<&User> {
        match self {
            Self::Ok(user) => Some(user),
            Self::Error(_) => None,
        }
    }

    fn error(&self) -> Option<&[ValidationError]> {
        match self {
            Self::Ok(_) => None,
            Self::Error(errs) => Some(errs.as_slice())
        }
    }
}

fn main() {}

Here, we use an enum to decide whether a client's input data is valid or not, and it could be used as the result of e.g. a signUp mutation:

{
  mutation {
    signUp(name: "wrong") {
      user {
          name
      }
      error {
          field
          message
      }
    }
  }
}

Generics

Yet another point where GraphQL and Rust differs is in how generics work:

  • In Rust, almost any type could be generic - that is, take type parameters.
  • In GraphQL, there are only two generic types: lists and non-nullables.

This poses a restriction on what we can expose in GraphQL from Rust: no generic structs can be exposed - all type parameters must be bound. For example, we cannot expose Result<T, E> as a GraphQL type, but we can expose Result<User, String> as a GraphQL type.

Let's make a slightly more compact but generic implementation of the last schema error example:

extern crate juniper;
use juniper::{graphql_object, GraphQLObject};

#[derive(GraphQLObject)] 
struct User { 
    name: String, 
}

#[derive(GraphQLObject)] 
struct ForumPost { 
    title: String,
}

#[derive(GraphQLObject)]
struct ValidationError {
    field: String,
    message: String,
}

struct MutationResult<T>(Result<T, Vec<ValidationError>>);

#[graphql_object]
#[graphql(name = "UserResult")]
impl MutationResult<User> {
    fn user(&self) -> Option<&User> {
        self.0.as_ref().ok()
    }

    fn error(&self) -> Option<&[ValidationError]> {
        self.0.as_ref().err().map(Vec::as_slice)
    }
}

#[graphql_object]
#[graphql(name = "ForumPostResult")]
impl MutationResult<ForumPost> {
    fn forum_post(&self) -> Option<&ForumPost> {
        self.0.as_ref().ok()
    }

    fn error(&self) -> Option<&[ValidationError]> {
        self.0.as_ref().err().map(Vec::as_slice)
    }
}

fn main() {}

Here, we've made a wrapper around a Result and exposed some concrete instantiations of Result<T, E> as distinct GraphQL objects.

NOTE: The reason we needed the wrapper is of Rust's orphan rules (both the Result and Juniper's internal traits are from third-party sources).

NOTE: Because we're using generics, we also need to specify a name for our instantiated GraphQL types. Even if Juniper could figure out the name, MutationResult<User> wouldn't be a valid GraphQL type name. And, also, two different GraphQL types cannot have the same MutationResult name, inferred by default.

Interfaces

GraphQL interfaces represent a list of named fields and their arguments. GraphQL objects and interfaces can then implement these interfaces which requires that the implementing type will define all fields defined by those interfaces.

GraphQL interfaces map well to interfaces known from common object-oriented languages such as Java or C#, but Rust, unfortunately, has no concept that maps perfectly to them. The nearest analogue of GraphQL interfaces are Rust traits, but the main difference is that in GraphQL an interface type serves both as an abstraction and a boxed value (dispatchable to concrete implementers), while in Rust, a trait is an abstraction only, and to represent such a boxed value a separate type is required, like a trait object or an enum consisting of implementer types, because Rust trait doesn't represent a type itself, and so, can have no values.

Another notable difference is that GraphQL interfaces are more like structurally-typed contracts: they only declare a list of fields a GraphQL type should already have. Rust traits, on the other hand, are type classes, which don't really care about existing methods, but, rather, require to provide implementations for required methods despite the fact whether the type already has such methods or not. This difference makes the trait implementation not a good fit for expressing a GraphQL interface implementation, because we don't really need to implement any fields, the GraphQL type implementing a GraphQL interface has those fields already. We only need to check that fields' signatures match.

That's why Juniper takes the following approach to represent GraphQL interfaces, which consists of two parts:

  1. Either a struct, or a trait (in case fields have arguments), which acts only as a blueprint describing the required list of fields, and is not used in runtime at all.
  2. An auto-generated enum, representing a dispatchable value-type for the GraphQL interfaces, which may be referred and returned by other fields.

This may be done by using either the #[graphql_interface] attribute or the #[derive(GraphQLInterface)]:

extern crate juniper;
use juniper::{graphql_interface, GraphQLInterface, GraphQLObject};

// By default a `CharacterValue` enum is generated by macro to represent
// values of this GraphQL interface.
#[derive(GraphQLInterface)]
#[graphql(for = Human)] // enumerating all implementers is mandatory
struct Character {
    id: String,
}

// Using a trait to describe the required fields is fine too. 
#[graphql_interface]
#[graphql(enum = HasHomeEnum, for = Human)]
//        ^^^^ the generated value-type enum can be renamed, if required
trait HasHome {
    fn home_planet(&self) -> &str;
}

#[derive(GraphQLObject)]
#[graphql(impl = [CharacterValue, HasHomeEnum])] 
//                ^^^^^^^^^^^^^^  ^^^^^^^^^^^
// Notice the enum type names, neither the trait name nor the struct name
// is used to refer the GraphQL interface.
struct Human {
  id: String,          // also resolves `Character.id` field
  home_planet: String, // also resolves `HasHome.homePlanet` field
}

fn main() {}

Interfaces implementing other interfaces

GraphQL allows implementing interfaces on other interfaces in addition to objects:

extern crate juniper;
use juniper::{graphql_object, GraphQLInterface, ID};

#[derive(GraphQLInterface)]
#[graphql(for = [HumanValue, Luke])]
struct Node {
    id: ID,
}

#[derive(GraphQLInterface)]
#[graphql(impl = NodeValue, for = Luke)]
struct Human {
    id: ID,
    home_planet: String,
}

struct Luke {
    id: ID,
}

#[graphql_object]
#[graphql(impl = [HumanValue, NodeValue])]
impl Luke {
    fn id(&self) -> &ID {
        &self.id
    }

    // As `String` and `&str` aren't distinguished by GraphQL spec and 
    // represent the same `!String` GraphQL scalar, we can use them 
    // interchangeably. The same is applied for `Cow<'a, str>`.
    //                  ⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄
    fn home_planet() -> &'static str {
        "Tatooine"
    }
}

fn main() {}

NOTE: Every interface has to specify all other interfaces/objects it implements or implemented for. Missing one of for = or impl = attribute arguments is a compile-time error.

extern crate juniper;
use juniper::{GraphQLInterface, GraphQLObject};

#[derive(GraphQLObject)]
pub struct ObjA {
    id: String,
}

#[derive(GraphQLInterface)]
#[graphql(for = ObjA)]
//        ^^^^^^^^^^ the evaluated program panicked at 
// 'Failed to implement interface `Character` on `ObjA`: missing interface 
//  reference in implementer's `impl` attribute.'
struct Character {
  id: String,
}

fn main() {}

Subtyping and additional nullable arguments

GraphQL allows implementers (both objects and other interfaces) to return "subtypes" instead of an original value. Basically, this allows to impose additional bounds on the implementation.

Valid "subtypes" are:

  • interface implementer instead of an interface itself:
    • I implements T in place of a T;
    • Vec<I implements T> in place of a Vec<T>.
  • non-null value in place of a nullable:
    • T in place of a Option<T>;
    • Vec<T> in place of a Vec<Option<T>>.

These rules are recursively applied, so Vec<Vec<I implements T>> is a valid "subtype" of a Option<Vec<Option<Vec<Option<T>>>>>.

Also, GraphQL allows implementers to add nullable field arguments, which aren't present on an original interface.

extern crate juniper;
use juniper::{graphql_interface, graphql_object, GraphQLInterface, ID};

#[derive(GraphQLInterface)]
#[graphql(for = [HumanValue, Luke])]
struct Node {
    id: ID,
}

#[derive(GraphQLInterface)]
#[graphql(for = HumanConnectionValue)]
struct Connection {
    nodes: Vec<NodeValue>,
}

#[derive(GraphQLInterface)]
#[graphql(impl = NodeValue, for = Luke)]
struct Human {
    id: ID,
    home_planet: String,
}

#[derive(GraphQLInterface)]
#[graphql(impl = ConnectionValue)]
struct HumanConnection {
    nodes: Vec<HumanValue>,
    //         ^^^^^^^^^^ notice not `NodeValue`
    // This can happen, because every `Human` is a `Node` too, so we just
    // impose additional bounds, which still can be resolved with
    // `... on Connection { nodes }` syntax.
}

struct Luke {
    id: ID,
}

#[graphql_object]
#[graphql(impl = [HumanValue, NodeValue])]
impl Luke {
    fn id(&self) -> &ID {
        &self.id
    }
    
    fn home_planet(language: Option<String>) -> &'static str {
        //                   ^^^^^^^^^^^^^^
        // Notice additional `null`able field argument, which is missing on 
        // `Human`. Resolving `...on Human { homePlanet }` will provide `None` 
        // for this argument (default argument value).
        match language.as_deref() {
            None | Some("en") => "Tatooine",
            Some("ko") => "타투인",
            _ => unimplemented!(),
        }
    }
}

fn main() {}

NOTE: Violating GraphQL "subtyping" or additional nullable argument rules is a compile-time error.

extern crate juniper;
use juniper::{graphql_object, GraphQLInterface};

pub struct ObjA {
    id: String,
}

#[graphql_object]
#[graphql(impl = CharacterValue)]
impl ObjA {
    fn id(&self, is_present: bool) -> &str {
//     ^^ the evaluated program panicked at 
// 'Failed to implement interface `Character` on `ObjA`: Field `id`: Argument 
//  `isPresent` of type `Boolean!` isn't present on the interface and so has
//  to be nullable.'        
        is_present.then_some(&self.id).unwrap_or("missing")
    }
}

#[derive(GraphQLInterface)]
#[graphql(for = ObjA)]
struct Character {
    id: String,
}

fn main() {}
extern crate juniper;
use juniper::{GraphQLInterface, GraphQLObject};

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
    id: Vec<String>,
//  ^^ the evaluated program panicked at 
// 'Failed to implement interface `Character` on `ObjA`: Field `id`: 
//  implementer is expected to return a subtype of interface's return
//  object: `[String!]!` is not a subtype of `String!`.'    
}

#[derive(GraphQLInterface)]
#[graphql(for = ObjA)]
struct Character {
    id: String,
}

fn main() {}

Default arguments

Similarly to GraphQL object fields, GraphQL arguments of interfaces are able to have default values, though Rust doesn't have such notion:

extern crate juniper;
use juniper::graphql_interface;

#[graphql_interface]
trait Person {
    fn field1(
        // Default value can be any valid Rust expression, including a function
        // call, etc.
        #[graphql(default = true)]
        arg1: bool,
        // If default expression is not specified, then the `Default::default()` 
        // value is used.
        #[graphql(default)]
        arg2: i32,
    ) -> String;
}

fn main() {}

Renaming

Just as with defining GraphQL objects, by default, fields are converted from Rust's standard snake_case naming convention into GraphQL's camelCase convention:

We can override the name by using the #[graphql(name = "...")] attribute:

extern crate juniper;
use juniper::{graphql_interface, GraphQLInterface};

#[derive(GraphQLInterface)]
#[graphql(name = "CharacterInterface")]
struct Character { // exposed as `CharacterInterface` in GraphQL schema
    #[graphql(name = "myCustomFieldName")]
    renamed_field: bool, // exposed as `myCustomFieldName` in GraphQL schema
}

#[graphql_interface]
#[graphql(name = "PersonInterface")]
trait Person { // exposed as `PersonInterface` in GraphQL schema
    #[graphql(name = "myCustomFieldName")]
    fn renamed_field( // exposed as `myCustomFieldName` in GraphQL schema
        #[graphql(name = "myArgument")]
        renamed_argument: bool, // exposed as `myArgument` in GraphQL schema
    ) -> bool;
}

fn main() {}

Or provide a different renaming policy for all the defined fields:

extern crate juniper;
use juniper::graphql_interface;

#[graphql_interface]
#[graphql(rename_all = "none")] // disables any renaming
trait Person {
    fn renamed_field( // exposed as `renamed_field` in GraphQL schema
        renamed_argument: bool, // exposed as `renamed_argument` in GraphQL schema
    ) -> bool;
}

fn main() {}

TIP: Supported policies are: SCREAMING_SNAKE_CASE, camelCase and none (disables any renaming).

Documentation and deprecation

Similarly, GraphQL fields of interfaces may also be documented and deprecated via #[graphql(description = "...")] and #[graphql(deprecated = "...")]/#[deprecated] attributes:

extern crate juniper;
use juniper::graphql_interface;

/// This doc comment is visible only in Rust API docs.
#[graphql_interface]
#[graphql(description = "This description overwrites the one from doc comment.")]
trait Person {
    /// This doc comment is visible only in Rust API docs.
    #[graphql(description = "This description is visible only in GraphQL schema.")]
    fn empty() -> &'static str;
    
    #[graphql(desc = "This description is visible only in GraphQL schema.")]
    //        ^^^^ shortcut for a `description` argument
    fn field(
        #[graphql(desc = "This description is visible only in GraphQL schema.")]
        arg: bool,
    ) -> bool;

    /// This doc comment is visible in both Rust API docs and GraphQL schema 
    /// descriptions.
    #[graphql(deprecated = "Just because.")]
    fn deprecated_graphql() -> bool;
    
    // Standard Rust's `#[deprecated]` attribute works too!
    #[deprecated(note = "Reason is optional, btw!")]
    fn deprecated_standard() -> bool; // has no description in GraphQL schema
}

fn main() {}

NOTE: Only GraphQL interface/object fields and GraphQL enum values can be deprecated.

Ignoring

By default, all struct fields or trait methods are considered as GraphQL fields. If a helper method is needed, or it should be ignored for some reason, then it should be marked with the #[graphql(ignore)] attribute:

#![allow(dead_code)]
extern crate juniper;
use std::marker::PhantomPinned;
use juniper::{graphql_interface, GraphQLInterface};

#[derive(GraphQLInterface)]
struct Character {
    id: i32,
    #[graphql(ignore)]
    _pin: PhantomPinned,
}

#[graphql_interface]
trait Person {
    fn name(&self) -> &str;

    fn age(&self) -> i32;

    #[graphql(ignore)]
    fn hidden_from_graphql(&self) {
        // Ignored methods are allowed to have a default implementation!
    }

    #[graphql(skip)]
    //        ^^^^ alternative naming, up to your preference
    fn also_hidden_from_graphql(&self);
}

fn main() {}

TIP: See more available features in the API docs of the #[graphql_interface] attribute.

Unions

GraphQL unions represent an object that could be one of a list of GraphQL object types, but provides for no guaranteed fields between those types. They also differ from interfaces in that object types declare what interfaces they implement, but are not aware of what unions contain them.

From the server's point of view, GraphQL unions are somewhat similar to interfaces: the main difference is that they don't contain fields on their own, and so, we only need to represent a value, dispatchable into concrete objects.

Obviously, the most straightforward approach to express GraphQL unions in Rust is to use enums. In Juniper this may be done by using #[derive(GraphQLInterface)] attribute on them:

extern crate derive_more;
extern crate juniper;
use derive_more::From;
use juniper::{GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
struct Droid {
    id: String,
    primary_function: String,
}

#[derive(From, GraphQLUnion)]
//       ^^^^ only for convenience, and may be omitted
enum Character {
    Human(Human),
    Droid(Droid),
}

fn main() {}

Renaming

Just as with renaming GraphQL objects, we can override the default union name by using the #[graphql(name = "...")] attribute:

extern crate juniper;
use juniper::{GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
struct Droid {
    id: String,
    primary_function: String,
}

#[derive(GraphQLUnion)]
#[graphql(name = "CharacterUnion")]
enum Character { // exposed as `CharacterUnion` in GraphQL schema
    Human(Human),
    Droid(Droid),
}

fn main() {}

NOTE: Unlike Rust enum variants, GraphQL union members don't have any special names aside from the ones provided by objects themselves, and so, obviously, cannot be renamed.

Documentation

Similarly to documenting GraphQL objects, we can document a GraphQL union via #[graphql(description = "...")] attribute or Rust doc comments:

extern crate juniper;
use juniper::{GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
struct Droid {
    id: String,
    primary_function: String,
}

/// This doc comment is visible in both Rust API docs and GraphQL schema 
/// descriptions.
#[derive(GraphQLUnion)]
enum Character {
    /// This doc comment is visible only in Rust API docs.
    Human(Human),
    /// This doc comment is visible only in Rust API docs.
    Droid(Droid),
}

/// This doc comment is visible only in Rust API docs.
#[derive(GraphQLUnion)]
#[graphql(description = "This description overwrites the one from doc comment.")]
//        ^^^^^^^^^^^ or `desc` shortcut, up to your preference
enum Person {
    /// This doc comment is visible only in Rust API docs.
    Human(Human),
}

fn main() {}

NOTE: Unlike Rust enum variants, GraphQL union members don't have any special constructors aside from the provided objects directly, and so, cannot be documented, but rather reuse object descriptions "as is".

Ignoring

In some rare situations we may want to omit exposing an enum variant in a GraphQL schema. Similarly to GraphQL enums, we can just annotate the variant with the #[graphql(ignore)] attribute.

As an example, let's consider the situation where we need to bind some type parameter T for doing interesting type-level stuff in our resolvers. To achieve this we need to have PhantomData<T>, but we don't want it exposed in the GraphQL schema.

extern crate derive_more;
extern crate juniper;
use std::marker::PhantomData;
use derive_more::From;
use juniper::{GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
struct Droid {
    id: String,
    primary_function: String,
}

#[derive(From, GraphQLUnion)]
enum Character<S> {
    Human(Human),
    Droid(Droid),
    #[from(ignore)]
    #[graphql(ignore)]  
    //        ^^^^^^ or `skip`, up to your preference
    _State(PhantomData<S>),
}

fn main() {}

WARNING: It's the library user's responsibility to ensure that ignored enum variant is never returned from resolvers, otherwise resolving the GraphQL query will panic in runtime.

TIP: See more available features in the API docs of the #[derive(GraphQLUnion)] attribute.

Enums

GraphQL enum types, like scalar types, also represent leaf values in a GraphQL type system. However enum types describe the set of possible values.

Enums are not references for a numeric value, but are unique values in their own right. They may serialize as a string: the name of the represented value.

With Juniper a GraphQL enum may be defined by using the #[derive(GraphQLEnum)] attribute on a Rust enum as long as its variants do not have any fields:

extern crate juniper;
use juniper::GraphQLEnum;

#[derive(GraphQLEnum)]
enum Episode {
    NewHope,
    Empire,
    Jedi,
}

fn main() {}

Renaming

By default, enum variants are converted from Rust's standard PascalCase naming convention into GraphQL's SCREAMING_SNAKE_CASE convention:

extern crate juniper;
use juniper::GraphQLEnum;

#[derive(GraphQLEnum)]
enum Episode {
    NewHope, // exposed as `NEW_HOPE` in GraphQL schema
    Empire,  // exposed as `EMPIRE` in GraphQL schema
    Jedi,    // exposed as `JEDI` in GraphQL schema
}

fn main() {}

We can override the name by using the #[graphql(name = "...")] attribute:

extern crate juniper;
use juniper::GraphQLEnum;

#[derive(GraphQLEnum)]
#[graphql(name = "WrongEpisode")] // now exposed as `WrongEpisode` in GraphQL schema
enum Episode {
    #[graphql(name = "LAST_HOPE")]
    NewHope, // exposed as `LAST_HOPE` in GraphQL schema
    Empire,
    Jedi,
}

fn main() {}

Or provide a different renaming policy for all the enum variants:

extern crate juniper;
use juniper::GraphQLEnum;

#[derive(GraphQLEnum)]
#[graphql(rename_all = "none")] // disables any renaming
enum Episode {
    NewHope, // exposed as `NewHope` in GraphQL schema
    Empire,  // exposed as `Empire` in GraphQL schema
    Jedi,    // exposed as `Jedi` in GraphQL schema
}

fn main() {}

TIP: Supported policies are: SCREAMING_SNAKE_CASE, camelCase and none (disables any renaming).

Documentation and deprecation

Just like when defining GraphQL objects, the GraphQL enum type and its values could be documented and deprecated via #[graphql(description = "...")] and #[graphql(deprecated = "...")]/#[deprecated] attributes:

extern crate juniper;
use juniper::GraphQLEnum;

/// This doc comment is visible only in Rust API docs.
#[derive(GraphQLEnum)]
#[graphql(description = "An episode of Star Wars")]
enum StarWarsEpisode {
    /// This doc comment is visible only in Rust API docs.
    #[graphql(description = "This description is visible only in GraphQL schema.")]
    NewHope,

    /// This doc comment is visible only in Rust API docs.
    #[graphql(desc = "Arguably the best one in the trilogy.")]
    //        ^^^^ shortcut for a `description` argument
    Empire,

    /// This doc comment is visible in both Rust API docs and GraphQL schema 
    /// descriptions.
    Jedi,
    
    #[deprecated(note = "Only visible in Rust.")]
    #[graphql(deprecated = "We don't really talk about this one.")]
    //        ^^^^^^^^^^ takes precedence over Rust's `#[deprecated]` attribute
    ThePhantomMenace, // has no description in GraphQL schema
}

fn main() {}

NOTE: Only GraphQL object/interface fields and GraphQL enum values can be deprecated.

Ignoring

By default, all enum variants are included in the generated GraphQL enum type as values. To prevent including a specific variant, annotate it with the #[graphql(ignore)] attribute:

#![allow(dead_code)]
extern crate juniper;
use juniper::GraphQLEnum;

#[derive(GraphQLEnum)]
enum Episode<T> {
    NewHope,
    Empire,
    Jedi,
    #[graphql(ignore)]
    Legends(T),   // cannot be queried from GraphQL
    #[graphql(skip)]
    //        ^^^^ alternative naming, up to your preference
    CloneWars(T), // cannot be queried from GraphQL
}

fn main() {}

TIP: See more available features in the API docs of the #[derive(GraphQLEnum)] attribute.

Input objects

Fields may accept arguments to configure their behavior. These inputs are often scalars or enums, but they sometimes need to represent more complex values.

A GraphQL input object defines a set of input fields; the input fields are either scalars, enums, or other input objects. This allows arguments to accept arbitrarily complex structs.

In Juniper, defining a GraphQL input object is quite straightforward and similar to how trivial GraphQL objects are defined - by using the #[derive(GraphQLInputObject)] attribute on a Rust struct:

#![allow(unused_variables)]
extern crate juniper;
use juniper::{graphql_object, GraphQLInputObject, GraphQLObject};

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

struct Root;
#[derive(GraphQLObject)] struct User { name: String }

#[graphql_object]
impl Root {
    fn users_at_location(coordinate: Coordinate, radius: f64) -> Vec<User> {
        // Send coordinate to database
        // ...
      unimplemented!()
    }
}

fn main() {}

Renaming

Just as with defining GraphQL objects, by default struct fields are converted from Rust's standard snake_case naming convention into GraphQL's camelCase convention:

extern crate juniper;
use juniper::GraphQLInputObject;

#[derive(GraphQLInputObject)]
struct Person {
    first_name: String, // exposed as `firstName` in GraphQL schema
    last_name: String,  // exposed as `lastName` in GraphQL schema
}

fn main() {}

We can override the name by using the #[graphql(name = "...")] attribute:

extern crate juniper;
use juniper::GraphQLInputObject;

#[derive(GraphQLInputObject)]
#[graphql(name = "WebPerson")] // now exposed as `WebPerson` in GraphQL schema
struct Person {
    name: String,
    age: i32,
    #[graphql(name = "websiteURL")]
    website_url: Option<String>, // now exposed as `websiteURL` in GraphQL schema
}

fn main() {}

Or provide a different renaming policy for all the struct fields:

extern crate juniper;
use juniper::GraphQLInputObject;

#[derive(GraphQLInputObject)]
#[graphql(rename_all = "none")] // disables any renaming
struct Person {
    name: String,
    age: i32,
    website_url: Option<String>, // exposed as `website_url` in GraphQL schema
}

fn main() {}

TIP: Supported policies are: SCREAMING_SNAKE_CASE, camelCase and none (disables any renaming).

Documentation

Similarly, GraphQL descriptions may be provided by either using Rust doc comments or with the #[graphql(description = "...")] attribute:

extern crate juniper;
use juniper::GraphQLInputObject;

/// This doc comment is visible only in Rust API docs.
#[derive(GraphQLInputObject)]
#[graphql(description = "This description is visible only in GraphQL schema.")]
struct Person {
    /// This doc comment is visible only in Rust API docs.
    #[graphql(desc = "This description is visible only in GraphQL schema.")]
    //        ^^^^ shortcut for a `description` argument
    name: String,

    /// This doc comment is visible in both Rust API docs and GraphQL schema 
    /// descriptions.
    age: i32,
}

fn main() {}

NOTE: As of October 2021 GraphQL specification, GraphQL input object's fields cannot be deprecated.

Ignoring

By default, all struct fields are included into the generated GraphQL input object type. To prevent inclusion of a specific field annotate it with the #[graphql(ignore)] attribute:

WARNING: Ignored fields must either implement Default or be annotated with the #[graphql(default = <expression>)] argument.

extern crate juniper;
use juniper::GraphQLInputObject;

enum System {
    Cartesian,
}

#[derive(GraphQLInputObject)]
struct Point2D {
    x: f64,
    y: f64,
    #[graphql(ignore, default = System::Cartesian)]
    //                ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // This attribute is required, as we need to be able to construct
    // a `Point2D` value from the `{ x: 0.0, y: 0.0 }` GraphQL input value,
    // received from client-side.
    system: System,
    // `Default::default()` value is used, if no 
    // `#[graphql(default = <expression>)]` is specified.
    #[graphql(skip)]
    //        ^^^^ alternative naming, up to your preference
    shift: f64, 
}

fn main() {}

TIP: See more available features in the API docs of the #[derive(GraphQLInputObject)] attribute.

Scalars

GraphQL scalars represent primitive leaf values in a GraphQL type system: numbers, strings, and booleans.

Built-in

Juniper provides support for all the built-in scalars.

Rust typesGraphQL scalar
boolBoolean
i32Int
f64Float
String, &strString
juniper::IDID

NOTE: ID scalar is defined in the GraphQL spec as a type that is serialized as a string, but can be parsed from both a string and an integer.

TIP: There is no built-in support for i64, u64, or other Rust integer types, as the GraphQL spec doesn't define any built-in scalars for them by default. Instead, to be supported, they should be defined as custom scalars in a GraphQL schema.

Custom

We can create custom scalars for other primitive values, but they are still limited in the data types for representation, and only introduce additional semantic meaning. This, also, often requires coordination with the client library, intended to consume the API we're building.

Custom scalars can be defined in Juniper by using either #[derive(GraphQLScalar)] or #[graphql_scalar] attributes, which do work pretty much the same way (except, #[derive(GraphQLScalar)] cannot be used on type aliases).

Transparent delegation

Quite often, we want to create a custom GraphQL scalar type by just wrapping an existing one, inheriting all its behavior. In Rust, this is often called as "newtype pattern". This may be achieved by providing a #[graphql(transparent)] attribute to the definition:

extern crate juniper;
use juniper::{graphql_scalar, GraphQLScalar};

#[derive(GraphQLScalar)]
#[graphql(transparent)]
pub struct UserId(i32);

// Using `#[graphql_scalar]` attribute here makes no difference, and is fully
// interchangeable with `#[derive(GraphQLScalar)]`. It's only up to the 
// personal preference - which one to use.
#[graphql_scalar]
#[graphql(transparent)]
pub struct MessageId {
  value: i32,
}

fn main() {}

That's it, now the UserId and MessageId scalars can be used in GraphQL schema.

We may also customize the definition, to provide more information about our custom scalar in GraphQL schema:

extern crate juniper;
use juniper::GraphQLScalar;

/// You can use a Rust doc comment to specify a description in GraphQL schema.
#[derive(GraphQLScalar)]
#[graphql(
    transparent,
    // Overwrite the name of this type in the GraphQL schema.
    name = "MyUserId",
    // Specifying a type description via attribute takes precedence over the
    // Rust doc comment, which allows to separate Rust API docs from GraphQL 
    // schema descriptions, if required.
    description = "Actual description.",
    // Optional specification URL.
    specified_by_url = "https://tools.ietf.org/html/rfc4122",
)]
pub struct UserId(String);

fn main() {}

Resolving

In case we need to customize resolving of a custom GraphQL scalar value (change the way it gets executed), the #[graphql(to_output_with = <fn path>)] attribute is the way to do so:

extern crate juniper;
use juniper::{GraphQLScalar, ScalarValue, Value};

#[derive(GraphQLScalar)]
#[graphql(to_output_with = to_output, transparent)]
struct Incremented(i32);

/// Increments [`Incremented`] before converting into a [`Value`].
fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> {
    let inc = v.0 + 1;
    Value::from(inc)
}

fn main() {}

Input value parsing

Customization of a custom GraphQL scalar value parsing is possible via #[graphql(from_input_with = <fn path>)] attribute:

extern crate juniper;
use juniper::{GraphQLScalar, InputValue, ScalarValue};

#[derive(GraphQLScalar)]
#[graphql(from_input_with = Self::from_input, transparent)]
struct UserId(String);

impl UserId {
    /// Checks whether the [`InputValue`] is a [`String`] beginning with `id: ` 
    /// and strips it.
    fn from_input<S>(input: &InputValue<S>) -> Result<Self, String> 
    where
        S: ScalarValue
    {
        input.as_string_value()
            .ok_or_else(|| format!("Expected `String`, found: {input}"))
            .and_then(|str| {
                str.strip_prefix("id: ")
                    .ok_or_else(|| {
                        format!(
                            "Expected `UserId` to begin with `id: `, \
                             found: {input}",
                        )
                    })
            })
            .map(|id| Self(id.to_owned()))
    }
}

fn main() {}

Token parsing

Customization of which tokens a custom GraphQL scalar type should be parsed from, is possible via #[graphql(parse_token_with = <fn path>)] or #[graphql(parse_token(<types>)] attributes:

extern crate juniper;
use juniper::{
    GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
    ScalarValue, ScalarToken, Value,
};

#[derive(GraphQLScalar)]
#[graphql(
    to_output_with = to_output,
    from_input_with = from_input,
    parse_token_with = parse_token,
)]
//  ^^^^^^^^^^^^^^^^ Can be replaced with `parse_token(String, i32)`, which
//                   tries to parse as `String` first, and then as `i32` if
//                   prior fails.
enum StringOrInt {
    String(String),
    Int(i32),
}

fn to_output<S: ScalarValue>(v: &StringOrInt) -> Value<S> {
    match v {
        StringOrInt::String(s) => Value::scalar(s.to_owned()),
        StringOrInt::Int(i) => Value::scalar(*i),
    }
}

fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<StringOrInt, String> {
    v.as_string_value()
        .map(|s| StringOrInt::String(s.into()))
        .or_else(|| v.as_int_value().map(StringOrInt::Int))
        .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
}

fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<S> {
    <String as ParseScalarValue<S>>::from_str(value)
        .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
}

fn main() {}

NOTE: Once we provide all 3 custom functions, there is no sense to follow newtype pattern anymore, as nothing left to inherit.

Full behavior

Instead of providing all custom functions separately, it's possible to provide a module holding the appropriate to_output(), from_input() and parse_token() functions via #[graphql(with = <module path>)] attribute:

extern crate juniper;
use juniper::{
    GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
    ScalarValue, ScalarToken, Value,
};

#[derive(GraphQLScalar)]
#[graphql(with = string_or_int)]
enum StringOrInt {
    String(String),
    Int(i32),
}

mod string_or_int {
    use super::*;

    pub(super) fn to_output<S: ScalarValue>(v: &StringOrInt) -> Value<S> {
        match v {
            StringOrInt::String(s) => Value::scalar(s.to_owned()),
            StringOrInt::Int(i) => Value::scalar(*i),
        }
    }

    pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<StringOrInt, String> {
        v.as_string_value()
            .map(|s| StringOrInt::String(s.into()))
            .or_else(|| v.as_int_value().map(StringOrInt::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
    }

    pub(super) fn parse_token<S: ScalarValue>(t: ScalarToken<'_>) -> ParseScalarResult<S> {
        <String as ParseScalarValue<S>>::from_str(t)
            .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(t))
    }
}

fn main() {}

A regular impl block is also suitable for that:

extern crate juniper;
use juniper::{
    GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
    ScalarValue, ScalarToken, Value,
};

#[derive(GraphQLScalar)]
// #[graphql(with = Self)] <- default behaviour, so can be omitted
enum StringOrInt {
    String(String),
    Int(i32),
}

impl StringOrInt {
    fn to_output<S: ScalarValue>(&self) -> Value<S> {
        match self {
            Self::String(s) => Value::scalar(s.to_owned()),
            Self::Int(i) => Value::scalar(*i),
        }
    }

    fn from_input<S>(v: &InputValue<S>) -> Result<Self, String>
    where
        S: ScalarValue
    {
        v.as_string_value()
            .map(|s| Self::String(s.into()))
            .or_else(|| v.as_int_value().map(Self::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
    }

    fn parse_token<S>(value: ScalarToken<'_>) -> ParseScalarResult<S>
    where
        S: ScalarValue
    {
        <String as ParseScalarValue<S>>::from_str(value)
            .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
    }
}

fn main() {}

At the same time, any custom function still may be specified separately, if required:

extern crate juniper;
use juniper::{
    GraphQLScalar, InputValue, ParseScalarResult, ScalarValue,
    ScalarToken, Value
};

#[derive(GraphQLScalar)]
#[graphql(
    with = string_or_int,
    parse_token(String, i32)
)]
enum StringOrInt {
    String(String),
    Int(i32),
}

mod string_or_int {
    use super::*;

    pub(super) fn to_output<S>(v: &StringOrInt) -> Value<S>
    where
        S: ScalarValue,
    {
        match v {
            StringOrInt::String(s) => Value::scalar(s.to_owned()),
            StringOrInt::Int(i) => Value::scalar(*i),
        }
    }

    pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<StringOrInt, String>
    where
        S: ScalarValue,
    {
        v.as_string_value()
            .map(|s| StringOrInt::String(s.into()))
            .or_else(|| v.as_int_value().map(StringOrInt::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
    }

    // No need in `parse_token()` function.
}

fn main() {}

TIP: See more available features in the API docs of the #[derive(GraphQLScalar)] and #[graphql_scalar] attributes.

Foreign

For implementing custom scalars on foreign types there is #[graphql_scalar] attribute.

NOTE: To satisfy orphan rules, we should provide a local ScalarValue implementation.

extern crate juniper;
mod date {
   pub struct Date;
   impl std::str::FromStr for Date {
       type Err = String;

       fn from_str(_value: &str) -> Result<Self, Self::Err> { 
           unimplemented!()
       }
   }

   impl std::fmt::Display for Date {
       fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
           unimplemented!()
       }
   }
}

use juniper::DefaultScalarValue as CustomScalarValue;
use juniper::{graphql_scalar, InputValue, ScalarValue, Value};

#[graphql_scalar(
    with = date_scalar, 
    parse_token(String),
    scalar = CustomScalarValue,
)]
//           ^^^^^^^^^^^^^^^^^ local `ScalarValue` implementation
type Date = date::Date;
//          ^^^^^^^^^^ type from another crate

mod date_scalar {
    use super::*;
  
    pub(super) fn to_output(v: &Date) -> Value<CustomScalarValue> {
        Value::scalar(v.to_string())
    }

    pub(super) fn from_input(v: &InputValue<CustomScalarValue>) -> Result<Date, String> {
      v.as_string_value()
          .ok_or_else(|| format!("Expected `String`, found: {v}"))
          .and_then(|s| s.parse().map_err(|e| format!("Failed to parse `Date`: {e}")))
    }
}

fn main() {}

Supported out-of-the-box

Juniper provides out-of-the-box GraphQL scalar implementations for some very common Rust crates. The types from these crates will be usable in your schemas automatically after enabling the correspondent self-titled Cargo feature.

Schema

Juniper follows a code-first approach to define a GraphQL schema.

TIP: For a schema-first approach, consider using a juniper-from-schema crate for generating a juniper-based code from a schema file.

GraphQL schema consists of three object types: a query root, a mutation root, and a subscription root.

The query root operation type must be provided and must be an Object type.

The mutation root operation type is optional; if it is not provided, the service does not support mutations. If it is provided, it must be an Object type.

Similarly, the subscription root operation type is also optional; if it is not provided, the service does not support subscriptions. If it is provided, it must be an Object type.

The query, mutation, and subscription root types must all be different types if provided.

In Juniper, the RootNode type represents a schema. When the schema is first created, Juniper will traverse the entire object graph and register all types it can find. This means that if we define a GraphQL object somewhere but never use or reference it, it won't be exposed in a GraphQL schema.

Both query and mutation objects are regular GraphQL objects, defined like any other object in Juniper. The mutation and subscription objects, however, are optional, since schemas can be read-only and do not require subscriptions.

TIP: If mutation/subscription functionality is not needed, consider using the predefined EmptyMutation/EmptySubscription types for stubbing them in a RootNode.

extern crate juniper;
use juniper::{
    graphql_object, EmptySubscription, FieldResult, GraphQLObject, RootNode,
};

#[derive(GraphQLObject)] 
struct User { 
    name: String,
}

struct Query;

#[graphql_object]
impl Query {
    fn user_with_username(username: String) -> FieldResult<Option<User>> {
        // Look up user in database...
      unimplemented!()
    }
}

struct Mutation;

#[graphql_object]
impl Mutation {
    fn sign_up_user(name: String, email: String) -> FieldResult<User> {
        // Validate inputs and save user in database...
      unimplemented!()
    }
}

type Schema = RootNode<'static, Query, Mutation, EmptySubscription>;

fn main() {}

NOTE: It's considered a good practice to name query, mutation, and subscription root types as Query, Mutation, and Subscription respectively.

The usage of subscriptions is a little different from the mutation and query objects, so they are discussed in the separate chapter.

Export

Many tools in GraphQL ecosystem require a schema definition to operate on. With Juniper we can export our GraphQL schema defined in Rust code either represented in the GraphQL schema language or in JSON.

SDL (schema definition language)

To generate an SDL (schema definition language) representation of a GraphQL schema defined in Rust code, the as_sdl() method should be used for the direct extraction (requires enabling the schema-language Juniper feature):

extern crate juniper;
use juniper::{
    graphql_object, EmptyMutation, EmptySubscription, FieldResult, RootNode,
};

struct Query;

#[graphql_object]
impl Query {
    fn hello(&self) -> FieldResult<&str> {
        Ok("hello world")
    }
}

fn main() {
    // Define our schema in Rust.
    let schema = RootNode::new(
        Query,
        EmptyMutation::<()>::new(),
        EmptySubscription::<()>::new(),
    );

    // Convert the Rust schema into the GraphQL SDL schema.
    let result = schema.as_sdl();

    let expected = "\
schema {
  query: Query
}

type Query {
  hello: String!
}
";
  #[cfg(not(target_os = "windows"))]
    assert_eq!(result, expected);
}

JSON

To export a GraphQL schema defined in Rust code as JSON (often referred to as schema.json), the specially crafted introspection query should be issued. Juniper provides a convenience introspect() function to introspect the entire schema, which result can be serialized into JSON:

extern crate juniper;
extern crate serde_json;
use juniper::{
    graphql_object, EmptyMutation, EmptySubscription, GraphQLObject,
    IntrospectionFormat, RootNode,
};

#[derive(GraphQLObject)]
struct Example {
    id: String,
}

struct Query;

#[graphql_object]
impl Query {
   fn example(id: String) -> Example {
       unimplemented!()
   }
}

type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>;

fn main() {
    // Run the built-in introspection query.
    let (res, _errors) = juniper::introspect(
        &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
        &(),
        IntrospectionFormat::default(),
    ).unwrap();

    // Serialize the introspection result into JSON.
    let json_result = serde_json::to_string_pretty(&res);
    assert!(json_result.is_ok());
}

TIP: We still can convert the generated JSON into a GraphQL schema language representation by using tools like graphql-json-to-sdl command line utility.

Subscriptions

GraphQL subscriptions are a way to push data from a server to clients requesting real-time messages from a server. Subscriptions are similar to queries in that they specify a set of fields to be delivered to a client, but instead of immediately returning a single answer a result is sent every time a particular event happens on a server.

In order to execute subscriptions in Juniper, we need a coordinator (spawning long-lived connections) and a GraphQL object with fields resolving into a Stream of elements which will then be returned to a client. The juniper_subscriptions crate provides a default implementation of these abstractions.

The subscription root is just a GraphQL object, similar to the query root and mutations root that we define for operations in our GraphQL schema. For subscriptions all fields should be async and return a Stream of some GraphQL type values, rather than direct values.

extern crate futures;
extern crate juniper;
use std::pin::Pin;
use futures::Stream;
use juniper::{graphql_object, graphql_subscription, FieldError};

#[derive(Clone)]
pub struct Database;

impl juniper::Context for Database {}

pub struct Query;

#[graphql_object]
#[graphql(context = Database)]
impl Query {
   fn hello_world() -> &'static str {
       "Hello World!"
   }
}

type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;

pub struct Subscription;

#[graphql_subscription]
#[graphql(context = Database)]
impl Subscription {
    // This subscription operation emits two values sequentially:
    // the `String`s "Hello" and "World!".
    async fn hello_world() -> StringStream {
        let stream = futures::stream::iter([
            Ok(String::from("Hello")),
            Ok(String::from("World!")),
        ]);
        Box::pin(stream)
    }
}

fn main () {}

Coordinator

GraphQL subscriptions require a bit more resources than regular queries and provide a great vector for DoS attacks. This can can bring down a server easily if not handled correctly. The SubscriptionCoordinator trait provides coordination logic to enable functionality like DoS attacks mitigation and resource limits.

The SubscriptionCoordinator contains the schema and can keep track of opened connections, handle subscription start and end, and maintain a global ID for each subscription. Each time a connection is established, the SubscriptionCoordinator spawns a [32], which handles a single connection, providing resolver logic for a client stream as well as reconnection and shutdown logic.

While we can implement SubscriptionCoordinator ourselves, Juniper contains a simple and generic implementation called Coordinator. The subscribe method returns a Future resolving into a Result<Connection, GraphQLError>, where Connection is a Stream of values returned by the operation, and a GraphQLError is the error when the subscription operation fails.

extern crate futures;
extern crate juniper;
extern crate juniper_subscriptions;
extern crate serde_json;
use std::pin::Pin;
use futures::{Stream, StreamExt as _};
use juniper::{
    http::GraphQLRequest,
    graphql_object, graphql_subscription, 
    DefaultScalarValue, EmptyMutation, FieldError, 
    RootNode, SubscriptionCoordinator,
};
use juniper_subscriptions::Coordinator;

#[derive(Clone)]
pub struct Database;

impl juniper::Context for Database {}

impl Database {
    fn new() -> Self {
        Self
    }
}

pub struct Query;

#[graphql_object]
#[graphql(context = Database)]
impl Query {
    fn hello_world() -> &'static str {
        "Hello World!"
    }
}

type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;

pub struct Subscription;

#[graphql_subscription]
#[graphql(context = Database)]
impl Subscription {
    async fn hello_world() -> StringStream {
        let stream = futures::stream::iter([
            Ok(String::from("Hello")), 
            Ok(String::from("World!")),
        ]);
        Box::pin(stream)
    }
}

type Schema = RootNode<'static, Query, EmptyMutation<Database>, Subscription>;

fn schema() -> Schema {
    Schema::new(Query, EmptyMutation::new(), Subscription)
}

async fn run_subscription() {
    let schema = schema();
    let coordinator = Coordinator::new(schema);
    let db = Database::new();

    let req: GraphQLRequest<DefaultScalarValue> = serde_json::from_str(
        r#"{
            "query": "subscription { helloWorld }"
        }"#,
    ).unwrap();
    
    let mut conn = coordinator.subscribe(&req, &db).await.unwrap();
    while let Some(result) = conn.next().await {
        println!("{}", serde_json::to_string(&result).unwrap());
    }
}

fn main() {}

WebSocket

For information about serving GraphQL subscriptions over WebSocket, see the "Serving" chapter.

Introspection

The schema introspection system is accessible from the meta-fields __schema and __type which are accessible from the type of the root of a query operation.

__schema: __Schema!
__type(name: String!): __Type

Like all meta-fields, these are implicit and do not appear in the fields list in the root type of the query operation.

GraphQL provides introspection, allowing to see what queries, mutations and subscriptions a GraphQL server supports at runtime.

Because introspection queries are just regular GraphQL queries, Juniper supports them natively. For example, to get all the names of the types supported, we could execute the following query against Juniper:

{
  __schema {
    types {
      name
    }
  }
}

Disabling

Disabling introspection in production is a widely debated topic, but we believe it’s one of the first things you can do to harden your GraphQL API in production.

Some security requirements and considerations may mandate to disable GraphQL schema introspection in production environments. In Juniper this can be achieved by using the RootNode::disable_introspection() method:

extern crate juniper;
use juniper::{
    graphql_object, graphql_vars, EmptyMutation, EmptySubscription, GraphQLError,
    RootNode,
};

pub struct Query;

#[graphql_object]
impl Query {
    fn some() -> bool {
        true
    }
}

type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>;

fn main() {
    let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new())
        .disable_introspection();

    let query = "query { __schema { queryType { name } } }";

    match juniper::execute_sync(query, None, &schema, &graphql_vars! {}, &()) {
        Err(GraphQLError::ValidationError(errs)) => {
            assert_eq!(
                errs.first().unwrap().message(),
                "GraphQL introspection is not allowed, but the operation contained `__schema`",
            );
        }
        res => panic!("expected `ValidationError`, returned: {res:#?}"),
    }
}

NOTE: Attempt to execute an introspection query results in validation error, rather than execution error.

Serving

Once we have built a GraphQL schema, the next obvious step would be to serve it, so clients can interact with our GraphQL API. Usually, GraphQL APIs are served via HTTP.

Web server frameworks

Though the juniper crate doesn't provide a built-in HTTP server, the surrounding ecosystem does.

Officially supported

Juniper officially supports the following widely used and adopted web server frameworks in Rust ecosystem:

See their API docs and usage examples (accessible from API docs) for further details of how they should be used.

NOTE: All the officially supported web server framework integrations provide a simple and convenient way for exposing GraphiQL and/or GraphQL Playground with the GraphQL schema along. These powerful tools ease the development process by enabling you to explore and send client requests to the GraphQL API under development.

WebSocket

NOTE: WebSocket is a crucial part for serving GraphQL subscriptions over HTTP.

There are two widely adopted protocols for serving GraphQL over WebSocket:

  1. Legacy graphql-ws GraphQL over WebSocket Protocol, formerly used by Apollo and the subscriptions-transport-ws npm package, and now being deprecated.
  2. New graphql-transport-ws GraphQL over WebSocket Protocol, provided by the graphql-ws npm package and being used by Apollo as for now.

In the Juniper ecosystem, both implementations are provided by the juniper_graphql_ws crate. Most of the officially supported web server framework integrations are able to serve a GraphQL schema over WebSocket (including subscriptions) and even support auto-negotiation of the correct protocol based on the Sec-Websocket-Protocol HTTP header value. See their API docs and usage examples (accessible from API docs) for further details of how to do so.

Batching

The GraphQL standard generally assumes that there will be one server request per each client operation to perform (such as a query or mutation). This is conceptually simple but potentially inefficient.

Some client libraries (such as apollo-link-batch-http) have the ability to batch operations in a single HTTP request to save network round-trips and potentially increase performance. There are some tradeoffs, though, that should be considered before batching operations.

Juniper's server integration crates support batching multiple operations in a single HTTP request out-of-the-box via JSON arrays. This makes them compatible with client libraries that support batch operations without any special configuration.

NOTE: If you use a custom server integration, it's not a hard requirement to support batching, as it's not a part of the official GraphQL specification.

Assuming an integration supports operations batching, for the following GraphQL query:

{
  hero {
    name
  }
}

The JSON data to POST for an individual request would be:

{
  "query": "{hero{name}}"
}

And the response would be in the form:

{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

However, if we want to run the same query twice in a single HTTP request, the batched JSON data to POST would be:

[
  {
    "query": "{hero{name}}"
  },
  {
    "query": "{hero{name}}"
  }
]

And then, the response would be in the following array form:

[
  {
    "data": {
      "hero": {
        "name": "R2-D2"
      }
    }
  },
  {
    "data": {
      "hero": {
        "name": "R2-D2"
      }
    }
  }
]

Advanced topics

The chapters below cover some more advanced topics.

Implicit and explicit null

GraphQL has two semantically different ways to represent the lack of a value:

  • Explicitly providing the literal value: null.
  • Implicitly not providing a value at all.

There are two ways that a client can submit a null value as an argument or a field in a GraphQL query:

  1. Either use an explicit null literal:
    {
      field(arg: null)
    }
    
  2. Or simply omit the argument, so the implicit default null value kicks in:
    {
      field
    }
    

There are some situations where it's useful to know which one exactly has been provided.

For example, let's say we have a function that allows users to perform a "patch" operation on themselves. Let's say our users can optionally have favorite and least favorite numbers, and the input for that might look like this:

/// Updates user attributes. Fields that are [`None`] are left as-is.
struct UserPatch {
    /// If [`Some`], updates the user's favorite number.
    favorite_number: Option<Option<i32>>,

    /// If [`Some`], updates the user's least favorite number.
    least_favorite_number: Option<Option<i32>>,
}

fn main() {}

To set a user's favorite number to 7, we would set favorite_number to Some(Some(7)). In GraphQL, that might look like this:

mutation { patchUser(patch: { favoriteNumber: 7 }) }

To unset the user's favorite number, we would set favorite_number to Some(None). In GraphQL, that might look like this:

mutation { patchUser(patch: { favoriteNumber: null }) }

And if we want to leave the user's favorite number alone, just set it to None. In GraphQL, that might look like this:

mutation { patchUser(patch: {}) }

The last two cases rely on being able to distinguish between explicit and implicit null.

Unfortunately, plain Option is not capable to distinguish them. That's why in Juniper, this can be done using the Nullable type:

extern crate juniper;
use juniper::{graphql_object, FieldResult, GraphQLInputObject, Nullable};

#[derive(GraphQLInputObject)]
struct UserPatchInput {
    favorite_number: Nullable<i32>,
    least_favorite_number: Nullable<i32>,
}

impl From<UserPatchInput> for UserPatch {
   fn from(input: UserPatchInput) -> Self {
      Self {
         // The `explicit()` function transforms the `Nullable` into an
         // `Option<Option<T>>` as expected by the business logic layer.
         favorite_number: input.favorite_number.explicit(),
         least_favorite_number: input.least_favorite_number.explicit(),
      }
   }
}

struct UserPatch {
    favorite_number: Option<Option<i32>>,
    least_favorite_number: Option<Option<i32>>,
}

struct Session;
impl Session {
    fn patch_user(&self, _patch: UserPatch) -> FieldResult<()> { Ok(()) }
}

struct Context {
    session: Session,
}
impl juniper::Context for Context {}

struct Mutation;

#[graphql_object]
#[graphql(context = Context)]
impl Mutation {
    fn patch_user(patch: UserPatchInput, ctx: &Context) -> FieldResult<bool> {
        ctx.session.patch_user(patch.into())?;
        Ok(true)
    }
}

fn main() {}

N+1 problem

A common issue with GraphQL server implementations is how the resolvers query their datasource. With a naive and straightforward approach we quickly run into the N+1 problem, resulting in a large number of unnecessary database queries or HTTP requests.

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate juniper;
use anyhow::anyhow;
use juniper::{graphql_object, GraphQLObject};

type CultId = i32;
type UserId = i32;

struct Repository;

impl juniper::Context for Repository {}

impl Repository {
    async fn load_cult_by_id(&self, cult_id: CultId) -> anyhow::Result<Option<Cult>> { unimplemented!() }
    async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
}

#[derive(GraphQLObject)]
struct Cult {
    id: CultId,
    name: String,
}

struct Person {
    id: UserId,
    name: String,
    cult_id: CultId,
}

#[graphql_object]
#[graphql(context = Repository)]
impl Person {
    fn id(&self) -> CultId {
        self.id
    }
    
    fn name(&self) -> &str {
        self.name.as_str()
    }
    
    async fn cult(&self, #[graphql(ctx)] repo: &Repository) -> anyhow::Result<Cult> {
        // Effectively performs the following SQL query:
        // SELECT id, name FROM cults WHERE id = ${cult_id} LIMIT 1
        repo.load_cult_by_id(self.cult_id)
            .await?
            .ok_or_else(|| anyhow!("No cult exists for ID `{}`", self.cult_id))
    }
}

struct Query;

#[graphql_object]
#[graphql(context = Repository)]
impl Query {
    async fn persons(#[graphql(ctx)] repo: &Repository) -> anyhow::Result<Vec<Person>> {
        // Effectively performs the following SQL query:
        // SELECT id, name, cult_id FROM persons
        repo.load_all_persons().await
    }
}
}

Let's say we want to list a bunch of cults persons were in:

query {
  persons {
    id
    name
    cult {
      id
      name
    }
  }
}

Once the persons list has been resolved, a separate SQL query is run to find the cult of each Person. We can see how this could quickly become a problem.

SELECT id, name, cult_id FROM persons;
SELECT id, name FROM cults WHERE id = 1;
SELECT id, name FROM cults WHERE id = 2;
SELECT id, name FROM cults WHERE id = 1;
SELECT id, name FROM cults WHERE id = 3;
SELECT id, name FROM cults WHERE id = 4;
SELECT id, name FROM cults WHERE id = 1;
SELECT id, name FROM cults WHERE id = 2;
-- and so on...

There are several ways how this problem may be resolved in Juniper. The most common ones are:

DataLoader

DataLoader pattern, named after the correspondent dataloader NPM package, represents a mechanism of batching and caching data requests in a delayed manner for solving the N+1 problem.

A port of the "Loader" API originally developed by @schrockn at Facebook in 2010 as a simplifying force to coalesce the sundry key-value store back-end APIs which existed at the time. At Facebook, "Loader" became one of the implementation details of the "Ent" framework, a privacy-aware data entity loading and caching layer within web server product code. This ultimately became the underpinning for Facebook's GraphQL server implementation and type definitions.

In Rust ecosystem, DataLoader pattern is introduced with the dataloader crate, naturally usable with Juniper.

Let's remake our example of N+1 problem, so it's solved by applying the DataLoader pattern:

extern crate anyhow;
extern crate dataloader;
extern crate juniper;
use std::{collections::HashMap, sync::Arc};
use anyhow::anyhow;
use dataloader::non_cached::Loader;
use juniper::{graphql_object, GraphQLObject};

type CultId = i32;
type UserId = i32;

struct Repository;

impl Repository {
    async fn load_cults_by_ids(&self, cult_ids: &[CultId]) -> anyhow::Result<HashMap<CultId, Cult>> { unimplemented!() }
    async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
}

struct Context {
    repo: Repository,
    cult_loader: CultLoader,
}

impl juniper::Context for Context {}

#[derive(Clone, GraphQLObject)]
struct Cult {
    id: CultId,
    name: String,
}

struct CultBatcher {
    repo: Repository,
}

// Since `BatchFn` doesn't provide any notion of fallible loading, like 
// `try_load()` returning `Result<HashMap<K, V>, E>`, we handle possible
// errors as loaded values and unpack them later in the resolver.
impl dataloader::BatchFn<CultId, Result<Cult, Arc<anyhow::Error>>> for CultBatcher {
    async fn load(
        &mut self, 
        cult_ids: &[CultId],
    ) -> HashMap<CultId, Result<Cult, Arc<anyhow::Error>>> {
        // Effectively performs the following SQL query:
        // SELECT id, name FROM cults WHERE id IN (${cult_id1}, ${cult_id2}, ...)
        match self.repo.load_cults_by_ids(cult_ids).await {
            Ok(found_cults) => {
                found_cults.into_iter().map(|(id, cult)| (id, Ok(cult))).collect()
            }
            // One could choose a different strategy to deal with fallible loads,
            // like consider values that failed to load as absent, or just panic.
            // See cksac/dataloader-rs#35 for details:
            // https://github.com/cksac/dataloader-rs/issues/35
            Err(e) => {
                // Since `anyhow::Error` doesn't implement `Clone`, we have to
                // work around here.
                let e = Arc::new(e);
                cult_ids.iter().map(|k| (k.clone(), Err(e.clone()))).collect()
            }
        }
    }
}

type CultLoader = Loader<CultId, Result<Cult, Arc<anyhow::Error>>, CultBatcher>;

fn new_cult_loader(repo: Repository) -> CultLoader {
    CultLoader::new(CultBatcher { repo })
        // Usually a `Loader` will coalesce all individual loads which occur 
        // within a single frame of execution before calling a `BatchFn::load()`
        // with all the collected keys. However, sometimes this behavior is not
        // desirable or optimal (perhaps, a request is expected to be spread out
        // over a few subsequent ticks).
        // A larger yield count will allow more keys to be appended to the batch,
        // but will wait longer before the actual load. For more details see:
        // https://github.com/cksac/dataloader-rs/issues/12 
        // https://github.com/graphql/dataloader#batch-scheduling
        .with_yield_count(100)
}

struct Person {
    id: UserId,
    name: String,
    cult_id: CultId,
}

#[graphql_object]
#[graphql(context = Context)]
impl Person {
    fn id(&self) -> CultId {
        self.id
    }
    
    fn name(&self) -> &str {
        self.name.as_str()
    }
    
    async fn cult(&self, ctx: &Context) -> anyhow::Result<Cult> {
        ctx.cult_loader
            // Here, we don't run the `CultBatcher::load()` eagerly, but rather
            // only register the `self.cult_id` value in the `cult_loader` and
            // wait for other concurrent resolvers to do the same.
            // The actual batch loading happens once all the resolvers register 
            // their IDs and there is nothing more to execute. 
            .try_load(self.cult_id)
            .await
            // The outer error is the `io::Error` returned by `try_load()` if
            // no value is present in the `HashMap` for the specified 
            // `self.cult_id`, meaning that there is no `Cult` with such ID
            // in the `Repository`.
            .map_err(|_| anyhow!("No cult exists for ID `{}`", self.cult_id))?
            // The inner error is the one returned by the `CultBatcher::load()`
            // if the `Repository::load_cults_by_ids()` fails, meaning that
            // running the SQL query failed.
            .map_err(|arc_err| anyhow!("{arc_err}"))
    }
}

struct Query;

#[graphql_object]
#[graphql(context = Context)]
impl Query {
    async fn persons(ctx: &Context) -> anyhow::Result<Vec<Person>> {
        // Effectively performs the following SQL query:
        // SELECT id, name, cult_id FROM persons
        ctx.repo.load_all_persons().await
    }
}

fn main() {
    
}

And now, performing a GraphQL query which lead to N+1 problem

query {
  persons {
    id
    name
    cult {
      id
      name
    }
  }
}

will lead to efficient SQL queries, just as expected:

SELECT id, name, cult_id FROM persons;
SELECT id, name FROM cults WHERE id IN (1, 2, 3, 4);

Caching

dataloader::cached provides a memoization cache: after BatchFn::load() is called once with given keys, the resulting values are cached to eliminate redundant loads.

DataLoader caching does not replace Redis, Memcached, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request.

WARNING: A DataLoader should be created per-request to avoid risk of bugs where one client is able to load cached/batched data from another client outside its authenticated scope. Creating a DataLoader within an individual resolver will prevent batching from occurring and will nullify any benefits of it.

Full example

For a full example using DataLoaders in Juniper check out the jayy-lmao/rust-graphql-docker repository.

Look-ahead

In backtracking algorithms, look ahead is the generic term for a subprocedure that attempts to foresee the effects of choosing a branching variable to evaluate one of its values. The two main aims of look-ahead are to choose a variable to evaluate next and to choose the order of values to assign to it.

In GraphQL, look-ahead machinery allows us to introspect the currently executed GraphQL operation to see which fields has been actually selected by it.

In Juniper, it's represented by the Executor::look_ahead() method.

#![allow(unused)]
fn main() {
extern crate juniper;
use juniper::{graphql_object, Executor, GraphQLObject, ScalarValue};

type UserId = i32;

#[derive(GraphQLObject)]
struct Person {
    id: UserId,
    name: String,
}

struct Query;

#[graphql_object]
// NOTICE: Specifying `ScalarValue` as custom named type parameter,
//         so its name is similar to the one used in methods.
#[graphql(scalar = S: ScalarValue)]
impl Query {
    fn persons<S: ScalarValue>(executor: &Executor<'_, '_, (), S>) -> Vec<Person> {
        // Let's see which `Person`'s fields were selected in the client query. 
        for field_name in executor.look_ahead().children().names() {
            dbg!(field_name);
        }
        // ...
      unimplemented!()
    }
}
}

TIP: S: ScalarValue type parameter on the method is required here to keep the Executor being generic over ScalarValue types. We, instead, could have used the DefaultScalarValue, which is the default ScalarValue type for the Executor, and make our code more ergonomic, but less flexible and generic.

#![allow(unused)]
fn main() {
extern crate juniper;
use juniper::{graphql_object, DefaultScalarValue, Executor, GraphQLObject};

type UserId = i32;

#[derive(GraphQLObject)]
struct Person {
    id: UserId,
    name: String,
}

struct Query;

#[graphql_object]
#[graphql(scalar = DefaultScalarValue)]
impl Query {
    fn persons(executor: &Executor<'_, '_, ()>) -> Vec<Person> {
        for field_name in executor.look_ahead().children().names() {
            dbg!(field_name);
        }
        // ...
      unimplemented!()
    }
}
}

N+1 problem

Naturally, look-ahead machinery allows us to solve the N+1 problem by introspecting the requested fields and performing loading in batches eagerly, before actual resolving of those fields:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate juniper;
use std::collections::HashMap;
use anyhow::anyhow;
use juniper::{graphql_object, Executor, GraphQLObject, ScalarValue};

type CultId = i32;
type UserId = i32;

struct Repository;

impl juniper::Context for Repository {}

impl Repository {
    async fn load_cult_by_id(&self, cult_id: CultId) -> anyhow::Result<Option<Cult>> { unimplemented!() }
    async fn load_cults_by_ids(&self, cult_ids: &[CultId]) -> anyhow::Result<HashMap<CultId, Cult>> { unimplemented!() }
    async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
}

enum Either<L, R> {
    Absent(L),
    Loaded(R),  
}

#[derive(Clone, GraphQLObject)]
struct Cult {
    id: CultId,
    name: String,
}

struct Person {
    id: UserId,
    name: String,
    cult: Either<CultId, Cult>,
}

#[graphql_object]
#[graphql(context = Repository)]
impl Person {
    fn id(&self) -> CultId {
        self.id
    }
    
    fn name(&self) -> &str {
        self.name.as_str()
    }
    
    async fn cult(&self, #[graphql(ctx)] repo: &Repository) -> anyhow::Result<Cult> {
        match &self.cult {
            Either::Loaded(cult) => Ok(cult.clone()),
            Either::Absent(cult_id) => {
                // Effectively performs the following SQL query:
                // SELECT id, name FROM cults WHERE id = ${cult_id} LIMIT 1
                repo.load_cult_by_id(*cult_id)
                    .await?
                    .ok_or_else(|| anyhow!("No cult exists for ID `{cult_id}`"))
            }
        }
    }
}

struct Query;

#[graphql_object]
#[graphql(context = Repository, scalar = S: ScalarValue)]
impl Query {
    async fn persons<S: ScalarValue>(
        #[graphql(ctx)] repo: &Repository,
        executor: &Executor<'_, '_, Repository, S>,
    ) -> anyhow::Result<Vec<Person>> {
        // Effectively performs the following SQL query:
        // SELECT id, name, cult_id FROM persons
        let mut persons = repo.load_all_persons().await?;
        
        // If the `Person.cult` field has been requested.
        if executor.look_ahead()
            .children()
            .iter()
            .any(|sel| sel.field_original_name() == "cult") 
        {
            // Gather `Cult.id`s to load eagerly.
            let cult_ids = persons
                .iter()
                .filter_map(|p| {
                    match &p.cult {
                        Either::Absent(cult_id) => Some(*cult_id),
                        // If for some reason a `Cult` is already loaded,
                        // then just skip it.
                        Either::Loaded(_) => None,
                    }
                })
                .collect::<Vec<_>>();
            
            // Load the necessary `Cult`s eagerly.
            // Effectively performs the following SQL query:
            // SELECT id, name FROM cults WHERE id IN (${cult_id1}, ${cult_id2}, ...)
            let cults = repo.load_cults_by_ids(&cult_ids).await?;
            
            // Populate `persons` with the loaded `Cult`s, so they do not perform
            // any SQL queries on resolving.
            for p in &mut persons {
                let Either::Absent(cult_id) = &p.cult else { continue; };
                p.cult = Either::Loaded(
                    cults.get(cult_id)
                        .ok_or_else(|| anyhow!("No cult exists for ID `{cult_id}`"))?
                        .clone(),
                );
            }
        }
        
        Ok(persons)
    }
}
}

And so, performing a GraphQL query which lead to N+1 problem

query {
  persons {
    id
    name
    cult {
      id
      name
    }
  }
}

will lead to efficient SQL queries, just as expected:

SELECT id, name, cult_id FROM persons;
SELECT id, name FROM cults WHERE id IN (1, 2, 3, 4);

More features

See more available look-ahead features in the API docs of the LookAheadSelection and the LookAheadChildren.

Eager loading

As a further evolution of the dealing with the N+1 problem via look-ahead, we may systematically remodel Rust types mapping to GraphQL ones in the way to encourage doing eager preloading of data for its fields and using the already preloaded data when resolving a particular field.

At the moment, this approach is represented with the juniper-eager-loading crate for Juniper.

NOTE: Since this library requires juniper-from-schema, it's best first to become familiar with it.

From "How this library works at a high level" and "A real example" sections of juniper-eager-loading documentation:

How this library works at a high level

If you have a GraphQL type like this

type User {
    id: Int!
    country: Country!
}

You might create the corresponding Rust model type like this:

#![allow(unused)]
fn main() {
struct User {
    id: i32,
    country_id: i32,
}
}

However this approach has one big issue. How are you going to resolve the field User.country without doing a database query? All the resolver has access to is a User with a country_id field. It can't get the country without loading it from the database...

Fundamentally these kinds of model structs don't work for eager loading with GraphQL. So this library takes a different approach.

What if we created separate structs for the database models and the GraphQL models? Something like this:

fn main() {}

mod models {
    pub struct User {
        id: i32,
        country_id: i32
    }

    pub struct Country {
        id: i32,
    }
}

struct User {
    user: models::User,
    country: HasOne<Country>,
}

struct Country {
    country: models::Country
}

enum HasOne<T> {
    Loaded(T),
    NotLoaded,
}

Now we're able to resolve the query with code like this:

  1. Load all the users (first query).
  2. Map the users to a list of country ids.
  3. Load all the countries with those ids (second query).
  4. Pair up the users with the country with the correct id, so change User.country from HasOne::NotLoaded to HasOne::Loaded(matching_country).
  5. When resolving the GraphQL field User.country simply return the loaded country.

A real example

use juniper::{Executor, FieldResult};
use juniper_eager_loading::{prelude::*, EagerLoading, HasOne};
use juniper_from_schema::graphql_schema;
use std::error::Error;

// Define our GraphQL schema.
graphql_schema! {
    schema {
        query: Query
    }

    type Query {
        allUsers: [User!]! @juniper(ownership: "owned")
    }

    type User {
        id: Int!
        country: Country!
    }

    type Country {
        id: Int!
    }
}

// Our model types.
mod models {
    use std::error::Error;
    use juniper_eager_loading::LoadFrom;

    #[derive(Clone)]
    pub struct User {
        pub id: i32,
        pub country_id: i32
    }

    #[derive(Clone)]
    pub struct Country {
        pub id: i32,
    }

    // This trait is required for eager loading countries.
    // It defines how to load a list of countries from a list of ids.
    // Notice that `Context` is generic and can be whatever you want.
    // It will normally be your Juniper context which would contain
    // a database connection.
    impl LoadFrom<i32> for Country {
        type Error = Box<dyn Error>;
        type Context = super::Context;

        fn load(
            employments: &[i32],
            field_args: &(),
            ctx: &Self::Context,
        ) -> Result<Vec<Self>, Self::Error> {
            // ...
            unimplemented!()
        }
    }
}

// Our sample database connection type.
pub struct DbConnection;

impl DbConnection {
    // Function that will load all the users.
    fn load_all_users(&self) -> Vec<models::User> {
        // ...
        unimplemented!()
    }
}

// Our Juniper context type which contains a database connection.
pub struct Context {
    db: DbConnection,
}

impl juniper::Context for Context {}

// Our GraphQL user type.
// `#[derive(EagerLoading)]` takes care of generating all the boilerplate code.
#[derive(Clone, EagerLoading)]
// You need to set the context and error type.
#[eager_loading(
    context = Context,
    error = Box<dyn Error>,

    // These match the default so you wouldn't have to specify them
    model = models::User,
    id = i32,
    root_model_field = user,
)]
pub struct User {
    // This user model is used to resolve `User.id`
    user: models::User,

    // Setup a "has one" association between a user and a country.
    //
    // We could also have used `#[has_one(default)]` here.
    #[has_one(
        foreign_key_field = country_id,
        root_model_field = country,
        graphql_field = country,
    )]
    country: HasOne<Country>,
}

// And the GraphQL country type.
#[derive(Clone, EagerLoading)]
#[eager_loading(context = Context, error = Box<dyn Error>)]
pub struct Country {
    country: models::Country,
}

// The root query GraphQL type.
pub struct Query;

impl QueryFields for Query {
    // The resolver for `Query.allUsers`.
    fn field_all_users(
        &self,
        executor: &Executor<'_, Context>,
        trail: &QueryTrail<'_, User, Walked>,
    ) -> FieldResult<Vec<User>> {
        let ctx = executor.context();

        // Load the model users.
        let user_models = ctx.db.load_all_users();

        // Turn the model users into GraphQL users.
        let mut users = User::from_db_models(&user_models);

        // Perform the eager loading.
        // `trail` is used to only eager load the fields that are requested. Because
        // we're using `QueryTrail`s from "juniper_from_schema" it would be a compile
        // error if we eager loaded associations that aren't requested in the query.
        User::eager_load_all_children_for_each(&mut users, &user_models, ctx, trail)?;

        Ok(users)
    }
}

impl UserFields for User {
    fn field_id(
        &self,
        executor: &Executor<'_, Context>,
    ) -> FieldResult<&i32> {
        Ok(&self.user.id)
    }

    fn field_country(
        &self,
        executor: &Executor<'_, Context>,
        trail: &QueryTrail<'_, Country, Walked>,
    ) -> FieldResult<&Country> {
        // This will unwrap the country from the `HasOne` or return an error if the
        // country wasn't loaded, or wasn't found in the database.
        Ok(self.country.try_unwrap()?)
    }
}

impl CountryFields for Country {
    fn field_id(
        &self,
        executor: &Executor<'_, Context>,
    ) -> FieldResult<&i32> {
        Ok(&self.country.id)
    }
}

fn main() {}

For more details, check out the juniper-eager-loading documentation.

Full example

For a full example using eager loading in Juniper check out the davidpdrsn/graphql-app-example repository.