Cynic

A bring your own types GraphQL client for Rust

Crate Info API Docs Discord Chat

Repository | Examples | Changelog

Overview

Cynic is a GraphQL library for Rust. It's not the first but it takes a different approach from the existing libraries.

Existing libraries take a query first approach to GQL - you write a query using GraphQL and libraries use that to generate Rust structs for you using macros. This is really easy and great for getting going quickly. However, if you want to use structs that aren't quite what the macros output you're out of luck. Some more complex use cases like sharing structs among queries are also commonly not supported.

Cynic takes a different approach - it uses Rust structs to define queries and generates GraphQL from them. This gives you freedom to control the structs you'll be working with while still enjoying type safe queries, checked against the GraphQL schema. When it's built in derives don't do exactly what you want it provides lower level APIs to hook in and fetch the data you want in the format you want to work with it.

Of course writing out all the structs to represent a large GraphQL query can be quite challenging, and GraphQL has excellent tooling for building queries usually. Cynic provides querygen to help with this - you write a GraphQL query using the existing GQL tooling and it'll generate some cynic structs to make that query. You can use this as a starting point for your projects - either adding on to the rust structs directly, or re-using querygen as appropriate.

Features

Cynic is currently a work in progress, but the following features are supported:

  • Typesafe queries & mutations.
  • Defining custom scalars.
  • Building dynamic (but still type checked) queries at run time.
  • Query arguments including input objects

The following features are not well supported or tested and may not work well, or at all:

  • Fetching union types via inline fragments

The following features are not yet supported, though should be soon (if you want to help out with the project I'd be happy for someone else to try and implement these - if you open an issue I'd be happy to give pointers on how to go about implementing any of them)

  • Fetching interface types.
  • GraphQL subscriptions.
  • Potentially other things (please open an issue if you find anything obviously missing)

Documentation

Cynic is documented in a few places:

  1. The guide that you're reading on cynic-rs.dev
  2. The reference documentation on docs.rs

Using This Guide

If you're new to Cynic the quickstart is a good place to start. Afterwards you might want to read the derives chapter, for more details about how to do common things with Cynic.

The Building Queries section is for more advanced users - either you've run into a case that the built in derives don't cover, or you're just curious how things work under the hood.

Inspiration

  • graphql-client, the original Rust GraphQL client. This is a great library for using GraphQL from Rust. It wasn't quite what I wanted but it might be what you want.
  • The idea of encoding the GraphQL typesystem into a DSL was taken from elm-graphql.
  • Most of the JSON decoding APIs were taken from Json.Decode in Elm.
  • Deriving code from structs is a fairly common Rust pattern, though serde in particular provided inspiration for the derive APIs.

Quickstart

If you just want to get going with cynic and don't care too much about how it works, this is the chapter for you.

Pre-requisites

There's a few things you'll need before you get started:

  1. An existing rust project (though you can just run cargo new if you don't have one).
  2. A GraphQL API that you'd like to query, and a copy of it's schema.
  3. A GraphQL query that you'd like to run against the API. If you don't have one of these, you should probably use graphiql or graphql playground to get started, or you can use the one I provide below. For this quickstart I'll be assuming you're using a query without any arguments.

Setting up dependencies.

First things first: you need to add cynic to your dependencies. We'll also need an HTTP client library. For the purposes of this quickstart we'll be using surf but you can use any library you want. Open up your Cargo.toml and add the following under the [dependencies] section:

cynic = { version = "0.10", features = ["surf"] }
surf = "2.0.0-alpha.7"

Note that we've added the surf feature flag of cynic - this pulls in some surf integration code, which we'll be using. If you're using a different HTTP client, you'll need a different feature flag or you may need to see the documentation for making an HTTP request manually.

You may also optionally want to install insta - a snapshot testing library that can be useful for double checking your GraphQL queries are as expected. Add this under [dev-dependencies] so it's available under test but not at runtime:

[dev-dependencies]
insta = "0.16"

Run a cargo check to make sure this builds and you're good to go.

Adding your schema to the build.

You'll want to make sure the GraphQL schema for your build is available to your builds. For example, you could put it at src/schema.graphql - the rest of this tutorial will assume that's where you put the schema.

Building your query structs.

Cynic allows you to build queries from Rust structs - so you'll need to take the query you're wanting to run, and convert it into one or more rust structs. This can be quite laborious and error prone for larger queries though so cynic provides querygen to help you get started.

Go to https://generator.cynic-rs.dev and select how you'd like to input your schema. If the GraphQL API you wish to use is accessible on the internet you can just link directly to it (although it will need to be have CORS headers enabled). Otherwise you can upload your schema to the generator.

Once you've provided the schema, you should be dropped into a GraphiQL interface but with an extra panel that contains Rust generated from your query & schema.

Checking your query (optional)

Since cynic generates queries for you based on Rust structs, it's not always obvious what the GraphQL queries look like. Sometimes you might want to run a query manually via Graphiql, or you might just want to see what effects changing the rust structs have on the query itself.

I find writing snapshot tests using insta useful for this purpose. Assuming your query is called AllFilmsQuery like mine is, you can add the following to the same file you put the struct output into:


#![allow(unused_variables)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn all_films_query_gql_output() {
        use cynic::QueryFragment;
        let query = cynic::Operation::query(AllFilmsQuery::fragment(&()));
        insta::assert_snapshot!(query.query);
    }
}
}

You can now run this test with cargo test. It should fail the first time, as you've not yet saved a snapshot. Run cargo insta review (you may need to cargo install insta first) to approve the snapshot, and the test should succeed.

You can use this snapshot test to double check the query whenever you make changes to the rust code, or just when you need some actual GraphQL to make a query outside of cynic.

Making your query

Now, you're ready to make a query against a server. Cynic doesn't provide any HTTP code for you, so you'll need to reach for your HTTP library of choice for this one. We'll use reqwest here, but it should be similar for any others.

First, you'll want to build a Query similar to how we did it in the snapshot test above (again, swapping AllFilmsQuery for the name of your root query struct):


#![allow(unused_variables)]
fn main() {
use cynic::QueryFragment;
let query = cynic::Operation::query(AllFilmsQuery::fragment(&()));
}

This Query struct is serializable using serde::Serialize, so you should pass it in as the HTTP body using your HTTP client and then make a request. For example, to use surf to talk to the StarWars API (see the docs for cynic::http if you're using another client):


#![allow(unused_variables)]
fn main() {
let response = surf::post("https://swapi-graphql.netlify.com/.netlify/functions/index")
    .run_graphql(&query)
    .await
    .unwrap();
}

Now, assuming everything went well, you should have the response to your query which you can do whatever you want with. And that's the end of the quickstart.

Tutorial

TODO

Deriving GraphQL Queries

Cynic has a few layers to it. The highest level of these is it's derives - using these derives you can define GraphQL queries using structs and enums, and have them type checked against the GraphQL schema itself.

There are a number of different derives which this section details:

Query Fragments

QueryFragments are the main tool for building queries & mutations in cynic. A QueryFragment tells cynic what fields to select from a GraphQL object, and how to decode those fields into a struct.

Generally you'll use a derive to create query fragments, like this:


#![allow(unused_variables)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    query_module = "query_dsl",
    graphql_type = "Film"
)]
struct Film {
    title: Option<String>,
    director: Option<String>,
}
}

When this struct is used in a query it will select the title & director fields of the Film type, which are both optional strings. QueryFragments can be nested inside each other, like so:


#![allow(unused_variables)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    query_module = "query_dsl",
    graphql_type = "Root",
)]
struct FilmsConnection {
    films: Vec<Film>,
}
}

If the above QueryFragment was used in a query, it would result in GraphQL that looked like:

films {
  title
  director
}

QueryFragments are compile time checked against the provided GraphQL schema. You cannot nest a Film QueryFragment into a field that was expecting an Actor for example. Similarly, nullable fields must be an Option and lists must be a Vec.

Making a Query with QueryFragments

QueryFragments that apply to the Query type (otherwise known as the Root type) of a schema can be used to build a cynic::Query. This is the type that can be sent to a server and used to decode the response.

If we wanted to use our FilmConnection to get all the films from the star wars API we need a QueryFragment like this:


#![allow(unused_variables)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    query_module = "query_dsl",
    graphql_type = "Root",
)]
struct AllFilmsQuery {
    all_films: Option<FilmConnection>,
}
}

This can be used as a query like so:

let query = cynic::Operation::query(AllFilmsQuery::fragment(&());

This Query can be converted into JSON using serde, sent to a server, and then then it's decode_response function can be used to decode the response itself. An example of this is in the Quickstart.

The empty () we pass to fragment is for the arguments - this particular query has no arguments so we pass Void.

Passing Arguments

To pass arguments into queries you must pass an argument_struct parameter to the cynic attribute, and then add arguments attributes to the fields for which you want to provide arugments. The argument_struct parameter must name a struct that implements cynic::FragmentArguments, which can also be derived. (See query arguments for more details)

Here, we define a query that fetches a film by a particular ID:


#![allow(unused_variables)]
fn main() {
#[derive(cynic::FragmentArguments)]
struct FilmArguments {
    id: Option<cynic::Id>,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    query_module = "query_dsl",
    graphql_type = "Root",
    argument_struct = "FilmArguments"
)]
struct FilmQuery {
    #[arguments(id = &args.id)]
    film: Option<Film>,
}
}

This can be converted into a query in a similar way we just need to provide the arguments:


#![allow(unused_variables)]
fn main() {
let query = cynic::Operation::query(FilmQuery::fragment(&FilmArguments{
    id: Some("ZmlsbXM6MQ==".into()),
}));
}

Nested Arguments

The example above showed how to pass arguments to the top level of a query. If you want to pass arguments to a nested QueryFragment then all it's parent QueryFragments must specify the same argument_struct in their cynic attribute. This is neccesary so that the FragmentArgument struct gets passed down to that level of a query.

If no nested QueryFragments require arguments, you can omit the argument_struct attr.

Mutations

Mutations are also constructed using QueryFragments in a very similar way to queries. Instead of selecting query fields you select a mutation, and pass in any arguments in exactly the same way. Instead of calling the Operation::query function to construct an Operation you call the Operation::mutation function.

Related

Enums

Much like with query structs cynic expects you to own any enums you want to query for, or provide as arguments. The cynic::Enum trait is used to define an enum, and the easiest way to define that trait is to derive it:


#![allow(unused_variables)]
fn main() {
#[derive(cynic::Enum, Clone, Copy, Debug)]
#[cynic(graphql_type = "Market")]
pub enum Market {
    UK,
    IE,
}
}

The derive will work on any enum that only has unit variants that match up with the variants on the enum in the schema. If there are any extra or missing variants, the derive will emit errors.

By default the variant names are expected to match the GraphQL variants exactly, but this can be controlled with either the rename_all top level parametr or the rename variant parameter.

Scalars

Cynic supports all the built in GraphQL scalars by default. If you want to query a field of one of these types add a field of the corresponding Rust type to your QueryFragment struct.

  • String fields in GraphQL should be String fields in Rust.
  • Int fields in GraphQL should be i32 in Rust.
  • Boolean fields in GraphQL map to bool in Rust.
  • ID fields in GraphQL map to the cynic::Id type in Rust.

Custom Scalars

GraphQL allows a schema to define it's own scalars - cynic also supports these. You can implement the Scalar trait manually, but it's recommended to use a derive:

#[derive(cynic::Scalar)]
struct MyScalar(String);

This defines a scalar called MyScalar - use this in a QueryFragment where you want to fetch a field of type MyScalar (which serializes to a String).

You can change the inner type that's used to serialize & deserialize the scalar by changing the type inside the struct.

Note that this derive only works on newtype structs - for any more complex datatype you'll have to implement cynic::Scalar yourself.

Query Arguments

A hierarchy of QueryFragments can take a struct of arguments. This struct must implement FragmentArguments which can be derived:

#[derive(cynic::FragmentArguments)]
struct FilmArguments {
    id: Option<cynic::Id>,
}

This derive can be used on any struct containing any fields - the fields do not need to be specifically related to GraphQL or used in a query, though if you don't use them at all you should get dead code warnings from Rust.

Using FragmentArguments

To use any fields of this struct as an argument to a QueryFragment, the struct must provide an argument_struct parameter that points to the FilmArguments struct. This allows arguments to be passed in using the arguments attribute on the fields where you wish to pass them.


#![allow(unused_variables)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    query_module = "query_dsl",
    graphql_type = "Root",
    argument_struct = "FilmArguments"
)]
struct FilmQuery {
    #[arguments(id = &args.id)]
    film: Option<Film>,
}
}

This example uses our FilmArguments at the root of the query to specify which film we want to fetch.

It's also possible to pass arguments down to lower levels of the query using the same technique. Though it's worth noting that all the QueryFragments from the Root to the point that requires arguments must define the same argument_struct in their cynic attribute. If no nested QueryFragments require any arguments then it's OK to omit argument_struct.

IntoArguments

Cynic uses the IntoArguments trait to convert arguments into the correct type. You can provide your own definition of this trait, but built in conversions are provided for:

  1. Converting bare scalars & enums into Options. This means you don't have to explicitly wrap an argument in Some. This also allows cynic to be tolerant of schemas changing a required argument into an optional argument (which would usually be considered a non-breaking change when your client in a dynamic language)
  2. Converting references to scalars & enums into owned arguments via clone. Cynic doesn't currently support taking arguments by reference, but this convenience saves users from having to explicitly clone.

InputObject

Some fields in GraphQL expect you to provide an input object rather than just simple scalar types. The cynic::InputObject trait is used to define an input object and the easiest way to define that trait is to derive it:


#![allow(unused_variables)]
fn main() {
#[derive(cynic::InputObject, Clone, Debug)]
#[cynic(graphql_type = "IssueOrder", rename_all = "camelCase")]
pub struct IssueOrder {
    pub direction: OrderDirection,
    pub field: IssueOrderField,
}
}

The derive will work on any struct that matches the format of the input object and it may contain scalar values or other InputObjects.

By default the field names are expected to match the GraphQL variants exactly, but this can be controlled with either the rename_all top level attribute or the rename attribute on individual fields.

If there are any fields in the struct that are not on the GraphQL input type the derive will emit errors. Any required fields that are on the GraphQL input type but not the rust struct will also error. Optional fields may be omitted without error. This maintains the same backwards compatability guarantees as most GraphQL clients: adding a required field is a breaking change, but adding an optional field is not.

Currently any missing optional fields will not be serialized in queries, whereas optional fields that are present in the struct but set to None will be sent as null.

Struct Attributes

An InputObject can be configured with several attributes on the struct itself:

  • graphql_type = "AType" is required and tells cynic which type in the GraphQL schema to map this struct to
  • rename_all="camelCase" tells cynic to rename all the rust field names with a particular rule to match their GraphQL counterparts. Typically this would be set to camelCase but others are supported.
  • require_all_fields can be provided when you want cynic to make sure your struct has all of the fields defined in the GraphQL schema.

Field Attributes

Each field can also have it's own attributes:

  • rename="someFieldName" can be used to map a field to a completely different GraphQL field name.
  • skip_serializing_if="path" can be used on optional fields to skip serializing them. By default an Option field will be sent as null to servers, but if you provide skip_serializing_if="Option::is_none" then the field will not be provided at all.

Inline Fragments

InlineFragments are used when querying an interface or union type, to determine which of the sub-types the query returned and to get at the fields inside that type.

InlineFragments can be derived on any enum that's variants contain a single type that implements QueryFragment. Each of the QueryFragments should have a graphql_type that maps to one of the sub-types of the graphql_type type for the InlineFragment.

For example, the GitHub API has an Assignee union type which could be queried with:


#![allow(unused_variables)]
fn main() {
#[derive(cynic::InlineFragments)]
#[cynic(
    schema_path = "github.graphql",
    query_module = "query_dsl",
    graphql_type = "Assignee"
)]
enum Assignee {
    Bot(Bot),
    Mannequin(Mannequin)
    Organization(Organization),
    User(User)
}
}

Where each of Bot, Mannequin, Organization & User are all structs that implement QueryFragment for the respective GraphQL types.

Building Queries Manually

The previous section of the book showed how to use the various derive macros cynic provides to build out GraphQL queries. If you've got a more complex use case, these might not always provide the functonality you're looking for. Cynic provides lower level functionality for building queries without these derives, and that's what we'll go into in this chapter.

This chapter is for advanced users of cynic, or those who're curious about how it works under the hood. If you're new to cynic or want the easiest path to making queries you probably want to go back to the Deriving GraphQL Queries section.

Selection Sets

Selection sets are involved in almost everything cynic does. A selection set contains a set of fields to fetch as part of a query, along with decoder functions that can decode the contents of those fields after they've been fetched.

When you derive a QueryFragment, cynic automatically creates a function that outputs a SelectionSet for the fields which you've put into your struct. That SelectionSet can then be added to SelectionSet of another QueryFragment if you nest it, or turned straight into a Query if you're at the root of the schema.

Selecting Scalars

The simplest selection sets in cynic are scalar selections - these simply tell cynic how to decode a field, but don't give any details about what field to decode. There are string, integer, float, boolean and more.

For example:

use cynic::selection_set::{bool, string};

// Will decode as a bool
let select_bool = bool();

// Will decode as a String:
let select_string = string()

Selecting Lists & Optionals

If you're wanting to fetch some optional or list fields, cynic provides the option & vec combinators respectively:

use cynic::selection_set::{string, bool, vec, option};

// Will decode as an Option<String>
let select_optional_string = option(string());

// Will decode as a Vec<bool>
let select_vec_bool = vec(bool());

Again, these selection sets don't know what field they're decoding from, only how to decode a field of a particular type.

Selecting Fields

On their own, the selection sets above are not very useful - you need to be able to apply them to a particular field. That's where the field function comes in:

use cynic::selection_set::{field, string};

// Selects a string from a "name" field
let select_name_field = field("name", vec![], string());

// Selects an optional integer from an "age" field
let select_age_field = field("age", vec![], option(integer());

Providing Arguments

The field function is also how you provide arguments to a GraphQL field - the second argument is a list of Argument structs to provide to the field in the query. To pass a parameter of adults: true to a field:

let select_people = field(
	"names",
	vec![Argument::new("adults", "Bool", true)],
	vec(string())
)

Note that you need to provide the GraphQL type name of the argument here.

Selecting Multiple Fields

Selecting an individual field is great but we need to be able to combine these individual field selections to build up an object. That's where the mapN functions come in - they allow you to combine a number of field selections into one, and pass the results of those selections to a function.

For example, to query for the name & age of a User:

struct User {
    name: String,
    age: Option<i32>
}

let select_user = map2(
	|(name, age)| User{ name, age },
	field("name", vec![], string()),
	field("age", vec![], option(integer());
);

This will select a String from a "name" field, and an Option from the age field and pass those into the closure we provided as the first argument. Since Rust doesn't have variadic functions there's a lot of these mapN functions, one for each possible number of arguments up to 50.

Selecting Nested Objects.

The third argument to the field function is just a SelectionSet, so you can combine the field & mapN functions to build up a nested query:

let select_query = field(
	"query", 
	vec![], 
	field("user", vec![], option(select_user()))
);

When this selection set is converted into a query, the GraphQL will look like:

query {
  user {
    name
    age
  }
}

The Query DSL

Writing Query Fragments

TODO

Sending HTTP Request Manually

The cynic::http module provides integrations for some HTTP clients, but sometimes you might want to make a request manually: either because you're using a client that cynic doesn't support, or the provided integrations just aren't sufficient in some way.

It's simple to make an HTTP query manually with cynic:

  • cynic::Operation implements serde::Serialize to build the body of a GraphQL request. This can be used with whatever JSON encoding functionality your HTTP client provides.
  • Once you've made the request, you should decode a cynic::GraphQLResponse<serde_json::Value> from the response, and then pass that to the decode_response function of your cynic::Operation.

For instance, to make a request with the reqwest::blocking client:


#![allow(unused_variables)]
fn main() {
use cynic::QueryFragment;
let query = cynic::Operation::query(AllFilmsQuery::fragment(&()));

let response = reqwest::blocking::Client::new()
    .post("https://swapi-graphql.netlify.com/.netlify/functions/index")
    .json(&query)
    .send()
    .unwrap();

let all_films_result = query.decode_response(response.json().unwrap()).unwrap();
}

Now you can do whatever you want with the result.