GraphQL in Rust
Expose flexible queries with async-graphql and Axum - schemas, resolvers, and DataLoader for N+1 prevention.
Recipe
Quick-reference recipe card - copy-paste ready.
use async_graphql::{Context, Object, Schema, SimpleObject};
#[derive(SimpleObject)]
struct User { id: i64, name: String }
struct Query;
#[Object]
impl Query {
async fn user(&self, ctx: &Context<'_>, id: i64) -> async_graphql::Result<User> {
Ok(User { id, name: "Ada".into() })
}
}
type AppSchema = Schema<Query, async_graphql::EmptyMutation, async_graphql::EmptySubscription>;When to reach for this: Clients need flexible field selection and nested graphs - mobile apps, BFFs, or admin explorers.
Working Example
use async_graphql::{Context, Object, Schema, SimpleObject};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{routing::post, Router};
#[derive(SimpleObject, Clone)]
struct User { id: i64, name: String }
#[derive(Clone)]
struct AppState { users: Vec<User> }
struct Query;
#[Object]
impl Query {
async fn users(&self, ctx: &Context<'_>) -> Vec<User> {
ctx.data_unchecked::<AppState>().users.clone()
}
}
type AppSchema = Schema<Query, async_graphql::EmptyMutation, async_graphql::EmptySubscription>;
async fn graphql_handler(
schema: axum::Extension<AppSchema>,
req: GraphQLRequest,
) -> GraphQLResponse {
schema.0.execute(req.into_inner()).await.into()
}
#[tokio::main]
async fn main() {
let state = AppState { users: vec![User { id: 1, name: "Ada".into() }] };
let schema = Schema::build(Query, async_graphql::EmptyMutation, async_graphql::EmptySubscription)
.data(state)
.finish();
let app = Router::new()
.route("/graphql", post(graphql_handler))
.layer(axum::Extension(schema));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}What this demonstrates:
#[Object]impl defines Query root resolvers.SimpleObjectfor output types.- Schema built with
.data(state)for resolver context. async_graphql_axumintegrates with Axum POST handler.
Deep Dive
How It Works
- Single
/graphqlendpoint accepts query document + variables JSON. - Resolvers async-fetch fields; executor batches where DataLoader configured.
- Introspection enables GraphiQL playground in dev.
GraphQL vs REST
| Topic | GraphQL | REST |
|---|---|---|
| Over-fetching | Client picks fields | Fixed response DTO |
| Versioning | Evolve schema carefully | URL /v1 |
| Caching | Harder at HTTP layer | GET cache friendly |
| Complexity | Query depth/cost limits needed | Simpler ops |
Rust Notes
// DataLoader for batched DB fetch
// use async_graphql::dataloader::LoaderGotchas
- N+1 without DataLoader - List users then query orders each. Fix: Loader per request.
- Unbounded query depth - Malicious nested query DOS. Fix:
limit_depthand complexity analysis. - Introspection in prod - Schema disclosure. Fix: Disable or auth-gate playground.
- Auth on field level - Forgotten checks on nested resolvers. Fix: Guard in each resolver or schema extensions.
- Error leakage - DB errors in GraphQL
errorsarray. Fix: Map to safe extensions only.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| REST + OpenAPI | Public simple CRUD | Highly variable client field needs |
| gRPC | Service mesh internal | Browser clients |
juniper | Older Juniper ecosystem | New projects - async-graphql default |
FAQs
Subscriptions?
WebSocket transport with async-graphql subscription root.
File uploads?
GraphQL multipart spec support in async-graphql.
Auth?
Extract bearer token in Axum middleware; insert AuthUser in schema data.
Persisted queries?
APQ for mobile clients to reduce payload size.
Codegen?
async-graphql exports schema SDL for frontend codegen.
Rate limit?
Query complexity score * cost budget per IP.
Testing?
schema.execute(Request::new(query)).await in unit tests.
REST coexistence?
Same Axum router - /graphql and /v1/* routes.
sqlx integration?
Pass PgPool in schema data; resolvers use sqlx directly.
When skip GraphQL?
Simple CRUD with few clients - REST is less operational burden.
Related
- API Design Basics - REST conventions
- Query Patterns & N+1 - DataLoader rationale
- Authentication & Sessions - Auth patterns
- Rate Limiting & Abuse Prevention - Query cost limits
- Input Validation & Injection - Untrusted queries
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.