rgoussu@goussu: ~/library/rust/protocols
~/library/rust/protocols cat graphql.md

GraphQL in Rust

# async-graphql as the modern default and juniper as the elder — code-first schemas from derives, dataloaders against N+1, subscriptions, and the client-side crates.

Conceptsaved 2026-08-09 #rust#protocols#graphql#async-graphql

Overview

Rust's GraphQL story is two code-first libraries: async-graphql — the modern default, async-native, spec-complete (federation, subscriptions, dataloaders) — and juniper, the older project that pioneered the space and still serves established codebases. Both derive the schema from Rust types, inverting the JVM's common schema-first flow: the SDL is generated output, and the compiler enforces resolver/type agreement by construction — there is no runtime "resolver missing for field" class of error.

Key points

  • async-graphql in one pass: #[derive(SimpleObject)] for plain object types, an Object impl block for types with resolver logic, #[derive(InputObject/Enum/ Interface/Union)] for the rest; a Schema<Query, Mutation, Subscription> served through first-class integrations for axum, actix-web, poem and warp.
  • Resolvers are async methods: arguments become GraphQL arguments, Context<'_> carries shared state (pools, loaders, auth) — the compile-time analog of graphql-java's runtime wiring.
  • N+1 is solved the standard way: dataloader::DataLoader batches key lookups per request tick — same pattern as Java's and Go's dataloaders, keyed and batched in Rust types.
  • Subscriptions are Stream-returning resolvers over WebSockets (graphql-ws protocol) — cheap on Tokio, wired via the same framework integrations; see WebSockets & SSE.
  • Federation: async-graphql implements Apollo Federation v2 directives (#[graphql(extends)], entity resolvers) — Rust subgraphs slot into a polyglot supergraph; Apollo's own router is itself written in Rust, a telling ecosystem fact.
  • juniper: same code-first shape, longer history, slower feature cadence (subscriptions and federation arrived late or partial) — choose it for existing codebases, async-graphql for new work.
  • Clients: graphql-client (derive typed queries from .graphql files + schema) and cynic (schema-first query DSL with compile-time validation) — both give the end-to-end typed pipeline GraphQL promises.
  • The guardrails are yours to add: depth/complexity limits (Schema::build(...).limit_depth(8).limit_complexity(200)), disabling introspection in production — same DoS-shaped concerns as every GraphQL server.

Examples

struct Query;

#[Object]
impl Query {
    async fn user(&self, ctx: &Context<'_>, id: Uuid) -> Result<Option<User>> {
        let loader = ctx.data_unchecked::<DataLoader<UserLoader>>();
        Ok(loader.load_one(id).await?)
    }
}

let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
    .data(DataLoader::new(UserLoader::new(pool), tokio::spawn))
    .limit_depth(8)
    .finish();
let app = Router::new().route("/graphql", post_service(GraphQL::new(schema)));

Related