A proof-of-concept GraphQL server framework for Rust

Recently, I've been working a new project, a framework for GraphQL server implementations in Rust. It's still very much at the proof of concept stage, but it is complete enough that I want to show it to the world. The main restriction is that it only works with a small subset of the GraphQL language. As far as I'm aware, it's the only framework which can provide an 'end to end' implementation of GraphQL in Rust (i.e., it handles IDL parsing, generates Rust code from IDL, and parses, validates, and executes queries).

The framework provides a seamless GraphQL interface for Rust servers. It is type-safe, ergonomic, very low boilerplate, and customisable. It has potential to be very fast. I believe that it can be one of the best experiences for GraphQL development in any language, as well as one of the fastest implementations (in part, because it seems to me that Rust and GraphQL are a great fit).

GraphQL

GraphQL is an interface for APIs. It's a query-based alternative to REST. A GraphQL API defines the structure of data, and clients can query that structured data. Compared to a traditional RESTful API, a GraphQL interface is more flexible and allows for clients and servers to evolve more easily and separately. Since a query returns exactly the data the client needs, there is less over-fetching or under-fetching of data, and fewer API calls. A good example of what a GraphQL API looks like is v4 of the GitHub API; see their blog post for a good description of GraphQL and why they chose if for their API.

Compared to a REST or other HTTP APIs, a GraphQL API takes a fair bit more setting up. Whereas, you can make a RESTful API from scratch with only a little work on top of most HTTP servers, to present a GraphQL API, you need to use a server library. That library takes care of understanding your data's schema, parsing and validating (effectively type-checking) queries against that schema, and orchestrating execution of queries (a good server framework also takes care of various optimisations such as caching query validation and batching parts of execution). The server developer plugs code into the framework as resolvers - code which implements 'small' functions in the schema.

Rust and GraphQL

Rust is a modern systems language, it offers a rich type system, memory safety without garbage collection, and an expanding set of libraries for writing very fast servers. I believe Rust is an excellent fit for implementing GraphQL servers. Over the past year or so, many developers have found Rust to be excellent for server-side software, and providing a good experience in that domain was a key goal for the Rust community in 2017.

Rust's data structures are a good match for GraphQL data structures, combined with a strong, safe, and static type system, this means that GraphQL implementations can be type-safe. Rust's powerful procedural macro system means that a GraphQL framework can do a lot of work at compile-time, and make implementation very ergonomic. Rust's trait system is more expressive and flexible than more common OO languages, and this allows for easy customisation of a GraphQL implementation. Finally Rust is fast because it can be low-level and supports many low-cost abstractions; it has emerging support for asynchronous programming which will work neatly for GraphQL resolvers.

Examples

I'm going to go through part of an example, the whole example is in the repo, and you can also see the full output from the schema macro.

The fundamental part of the framework is the schema macro, this is used to specify a GraphQL schema using IDL (not officially part of GraphQL, but widely used). Here's a small schema:

schema! {
    type Query {
        hero(episode: Episode): Character,
    }

    enum Episode {
        NEWHOPE,
        EMPIRE,
        JEDI,
    }

    type Human implements Character {
        id: ID!,  // `!` means non-null
        name: String!,
        friends: [Character],
        // ...
    }
}

This generates a whole bunch of code, but some interesting bits are:

// Rust types corresponding to `Episode` and `Human`:
#[allow(non_snake_case)]
#[derive(Clone, Debug)]
pub enum Episode {
    NEWHOPE,
    EMPIRE,
    JEDI,
}

#[allow(non_snake_case)]
#[derive(Clone, Debug)]
pub struct Human {
    pub id: Id,
    pub name: String,
    // Uses `Option` because GraphQL accepts `null` here.
    pub friends: Option<Vec<Option<Character>>>,
    // ...
}

// A trait for the `Query` type which the developer must implement.
// Note that it is actually more complex than this, see below.
pub trait AbstractQuery: ResolveObject {
    fn hero(&self, episode: Episode) -> QlResult<Option<Character>>;
}

In the simplest case, a user of the framework would just implement the AbstractQuery trait using the data types created by the schema macro. There is a little bit of glue code (basically implementing a 'root' trait), and you have a working GraphQL server!

By using a strongly typed language and by having Rust types for every GraphQL type, we ensure type safety for our GraphQL implementations. By making use of Rust's procedural macros, we minimise the amount of boilerplate a developer has to write to get to a working server. Combined with the kind of speed which a good Rust implementation could offer, I believe this provides an un-paralleled mix of ease of development and speed.

Sometimes though you don't need or want to use such data structures. For example, you might be getting data straight from a database and constructing a Rust object only to serialise it again; or you might be able to optimise a particularly common query by not providing all the fields of a given data type (e.g., perhaps Human::friends is in a separate DB table and we often only want a Humans name and id, then we might be able to save a lookup in the friends table by using a custom data structure).

In such cases the framework lets you easily customise the concrete data types. As well as generating the above types, the schema macro generates abstract versions of each data type as a trait, for example:

pub trait AbstractHuman: ::graphql::types::schema::ResolveObject {
    type Character: AbstractCharacter = Character;
    #[allow(non_snake_case)]
    fn to_Character(&self) -> QlResult<Self::Character>;
}

you can then implement AbstractHuman and ResolveObject for your custom type (there are some even more abstract traits that you almost certainly don't want to override, you need to implement them to, but the schema macro generates another macro to do that for you, so all it takes is the line ImplHuman!(MyCustomHumanType);). It is the ResolveObject implementation where you need to put some effort in to provide a resolve_field method.

Finally, you need to specify the custom types you are using. That is done using associated types in various traits, for example the associated type Character in AbstractHuman. There are similar associated types in the AbstractQuery trait, and the signature of hero uses those rather than the concrete Episode and Character types (i.e., I cheated a little bit in the definition of AbstractQuery above).

Although this is quite complex, I want to stress that in the common case (using the default, generated data types), you don't have to worry about any of this and it all happens auto-magically. However, when you need the power to customise the implementation, it is there for you.

The future

I'm pretty proud of the framework so far, but there is an awful lot still to do. The good news is that it seems like it will be really interesting, fun work! I've started filing issues on the repo. Some of the biggies are:

  • schema validation (we currently validate queries, but not schemas, which means we can crash rather than give an error message if there is a bad schema),
  • completeness (GraphQL is a pretty big language and only a small portion is implemented so far, lots to do!),
  • use asynchronous resolvers (each resolver might access a database or other server, so should be executed asynchronously. We should use Tokio and Rust futures to make this ergonomic and super-fast. There is also some orchestration work to do),
  • query validation caching and other fundamental optimisations (required to be an industrial-strength GraphQL server).

I'd love to have help doing this work and to build a community around GraphQL in Rust. If you're interested in hacking on the project, or considering using GraphQL and Rust in a project, please get in touch (twitter: @nick_r_cameron, irc: nrc, email: my irc nick at mozilla.com).