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, anObjectimpl block for types with resolver logic,#[derive(InputObject/Enum/ Interface/Union)]for the rest; aSchema<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::DataLoaderbatches 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
.graphqlfiles + 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
- Backend protocols in Rust — the map — parent map.
- REST in Rust — the default this augments when clients need flexible aggregation.
- WebSockets & SSE in Rust — the subscription transport.
- GraphQL on the JVM and GraphQL in Go — the contrasts: graphql-java's runtime wiring, gqlgen's schema-first codegen; Rust's derives are a third way.