Introdução
GraphQL se consolidou como uma alternativa poderosa ao REST para APIs modernas, e Rust oferece implementações que são ao mesmo tempo type-safe e extremamente performáticas. Enquanto servidores GraphQL em Node.js ou Python dependem de resolução dinâmica e introspecção em runtime, as bibliotecas Rust aproveitam o sistema de tipos da linguagem para validar o schema inteiro em tempo de compilação, eliminando uma classe inteira de erros que só apareceriam em produção com outras linguagens.
As duas principais bibliotecas são async-graphql (mais popular, abordagem code-first com macros) e Juniper (mais antiga, com suporte a schema-first). Neste artigo, vamos focar em async-graphql por sua ergonomia, performance e recursos avançados como subscriptions, DataLoader e integração nativa com Axum. Se você ainda está escolhendo seu framework web, veja nossa comparação entre Axum e Actix Web.
Async-graphql vs Juniper
| Aspecto | async-graphql | Juniper |
|---|---|---|
| Abordagem | Code-first (macros derive) | Code-first + schema-first |
| Async | Nativo (tokio) | Suportado |
| Subscriptions | WebSocket nativo | Suporte limitado |
| DataLoader | Integrado | Manual |
| Integração web | Axum, Actix, Rocket, Warp | Axum, Actix, Rocket, Warp |
| Popularidade | Mais popular (mais downloads) | Mais antigo, estável |
| Documentação | Muito boa | Boa |
Configuração do Projeto
[package]
name = "api-graphql"
version = "0.1.0"
edition = "2021"
[dependencies]
async-graphql = { version = "7", features = ["dataloader"] }
async-graphql-axum = "7"
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
Exemplo Prático: API GraphQL para Blog
Vamos construir uma API completa com queries, mutations, subscriptions e DataLoader para um sistema de blog.
Definição dos Tipos
use async_graphql::*;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Representa um autor do blog.
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
#[graphql(complex)]
pub struct Autor {
pub id: Uuid,
pub nome: String,
pub email: String,
pub bio: Option<String>,
pub criado_em: NaiveDateTime,
}
/// Campos computados do Autor (resolvidos via DataLoader).
#[ComplexObject]
impl Autor {
/// Retorna todos os posts deste autor.
async fn posts(&self, ctx: &Context<'_>) -> Result<Vec<Post>> {
let db = ctx.data::<sqlx::PgPool>()?;
let posts = sqlx::query_as!(
Post,
r#"SELECT id, titulo, conteudo, publicado,
autor_id, criado_em, atualizado_em
FROM posts WHERE autor_id = $1
ORDER BY criado_em DESC"#,
self.id
)
.fetch_all(db)
.await?;
Ok(posts)
}
/// Total de posts do autor.
async fn total_posts(&self, ctx: &Context<'_>) -> Result<i64> {
let db = ctx.data::<sqlx::PgPool>()?;
let resultado = sqlx::query_scalar!(
"SELECT COUNT(*) FROM posts WHERE autor_id = $1",
self.id
)
.fetch_one(db)
.await?;
Ok(resultado.unwrap_or(0))
}
}
/// Representa um post do blog.
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
#[graphql(complex)]
pub struct Post {
pub id: Uuid,
pub titulo: String,
pub conteudo: String,
pub publicado: bool,
pub autor_id: Uuid,
pub criado_em: NaiveDateTime,
pub atualizado_em: NaiveDateTime,
}
#[ComplexObject]
impl Post {
/// Retorna o autor deste post (usando DataLoader para evitar N+1).
async fn autor(&self, ctx: &Context<'_>) -> Result<Option<Autor>> {
let loader = ctx.data::<DataLoader<AutorLoader>>()?;
let autor = loader.load_one(self.autor_id).await?;
Ok(autor)
}
/// Retorna os comentários deste post.
async fn comentarios(&self, ctx: &Context<'_>) -> Result<Vec<Comentario>> {
let db = ctx.data::<sqlx::PgPool>()?;
let comentarios = sqlx::query_as!(
Comentario,
r#"SELECT id, conteudo, autor_nome, post_id, criado_em
FROM comentarios WHERE post_id = $1
ORDER BY criado_em ASC"#,
self.id
)
.fetch_all(db)
.await?;
Ok(comentarios)
}
}
/// Comentário em um post.
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct Comentario {
pub id: Uuid,
pub conteudo: String,
pub autor_nome: String,
pub post_id: Uuid,
pub criado_em: NaiveDateTime,
}
DataLoader para Evitar N+1
use async_graphql::dataloader::Loader;
use std::collections::HashMap;
pub struct AutorLoader {
pub pool: sqlx::PgPool,
}
impl Loader<Uuid> for AutorLoader {
type Value = Autor;
type Error = Arc<sqlx::Error>;
async fn load(
&self,
keys: &[Uuid],
) -> Result<HashMap<Uuid, Self::Value>, Self::Error> {
let autores = sqlx::query_as!(
Autor,
r#"SELECT id, nome, email, bio, criado_em
FROM autores WHERE id = ANY($1)"#,
keys
)
.fetch_all(&self.pool)
.await
.map_err(Arc::new)?;
Ok(autores.into_iter().map(|a| (a.id, a)).collect())
}
}
use std::sync::Arc;
Queries e Mutations
use async_graphql::*;
use uuid::Uuid;
pub struct QueryRoot;
#[Object]
impl QueryRoot {
/// Busca todos os posts publicados.
async fn posts(
&self,
ctx: &Context<'_>,
#[graphql(default = 20)] limite: i64,
#[graphql(default = 0)] offset: i64,
) -> Result<Vec<Post>> {
let db = ctx.data::<sqlx::PgPool>()?;
let posts = sqlx::query_as!(
Post,
r#"SELECT id, titulo, conteudo, publicado,
autor_id, criado_em, atualizado_em
FROM posts WHERE publicado = true
ORDER BY criado_em DESC
LIMIT $1 OFFSET $2"#,
limite,
offset
)
.fetch_all(db)
.await?;
Ok(posts)
}
/// Busca um post pelo ID.
async fn post(&self, ctx: &Context<'_>, id: Uuid) -> Result<Option<Post>> {
let db = ctx.data::<sqlx::PgPool>()?;
let post = sqlx::query_as!(
Post,
r#"SELECT id, titulo, conteudo, publicado,
autor_id, criado_em, atualizado_em
FROM posts WHERE id = $1"#,
id
)
.fetch_optional(db)
.await?;
Ok(post)
}
/// Busca posts por texto (título ou conteúdo).
async fn buscar_posts(
&self,
ctx: &Context<'_>,
termo: String,
) -> Result<Vec<Post>> {
let db = ctx.data::<sqlx::PgPool>()?;
let termo_busca = format!("%{}%", termo);
let posts = sqlx::query_as!(
Post,
r#"SELECT id, titulo, conteudo, publicado,
autor_id, criado_em, atualizado_em
FROM posts
WHERE publicado = true
AND (titulo ILIKE $1 OR conteudo ILIKE $1)
ORDER BY criado_em DESC"#,
termo_busca
)
.fetch_all(db)
.await?;
Ok(posts)
}
/// Retorna todos os autores.
async fn autores(&self, ctx: &Context<'_>) -> Result<Vec<Autor>> {
let db = ctx.data::<sqlx::PgPool>()?;
let autores = sqlx::query_as!(
Autor,
"SELECT id, nome, email, bio, criado_em FROM autores ORDER BY nome"
)
.fetch_all(db)
.await?;
Ok(autores)
}
}
// === Inputs ===
#[derive(InputObject)]
pub struct NovoPostInput {
pub titulo: String,
pub conteudo: String,
pub autor_id: Uuid,
#[graphql(default = false)]
pub publicado: bool,
}
#[derive(InputObject)]
pub struct AtualizarPostInput {
pub titulo: Option<String>,
pub conteudo: Option<String>,
pub publicado: Option<bool>,
}
#[derive(InputObject)]
pub struct NovoComentarioInput {
pub conteudo: String,
pub autor_nome: String,
pub post_id: Uuid,
}
// === Mutations ===
pub struct MutationRoot;
#[Object]
impl MutationRoot {
/// Cria um novo post.
async fn criar_post(
&self,
ctx: &Context<'_>,
input: NovoPostInput,
) -> Result<Post> {
let db = ctx.data::<sqlx::PgPool>()?;
let id = Uuid::new_v4();
let agora = chrono::Utc::now().naive_utc();
let post = sqlx::query_as!(
Post,
r#"INSERT INTO posts (id, titulo, conteudo, publicado, autor_id, criado_em, atualizado_em)
VALUES ($1, $2, $3, $4, $5, $6, $6)
RETURNING id, titulo, conteudo, publicado, autor_id, criado_em, atualizado_em"#,
id,
input.titulo,
input.conteudo,
input.publicado,
input.autor_id,
agora
)
.fetch_one(db)
.await?;
Ok(post)
}
/// Atualiza um post existente.
async fn atualizar_post(
&self,
ctx: &Context<'_>,
id: Uuid,
input: AtualizarPostInput,
) -> Result<Post> {
let db = ctx.data::<sqlx::PgPool>()?;
let agora = chrono::Utc::now().naive_utc();
let post = sqlx::query_as!(
Post,
r#"UPDATE posts SET
titulo = COALESCE($2, titulo),
conteudo = COALESCE($3, conteudo),
publicado = COALESCE($4, publicado),
atualizado_em = $5
WHERE id = $1
RETURNING id, titulo, conteudo, publicado, autor_id, criado_em, atualizado_em"#,
id,
input.titulo,
input.conteudo,
input.publicado,
agora
)
.fetch_one(db)
.await?;
Ok(post)
}
/// Adiciona um comentário a um post.
async fn comentar(
&self,
ctx: &Context<'_>,
input: NovoComentarioInput,
) -> Result<Comentario> {
let db = ctx.data::<sqlx::PgPool>()?;
let id = Uuid::new_v4();
let agora = chrono::Utc::now().naive_utc();
let comentario = sqlx::query_as!(
Comentario,
r#"INSERT INTO comentarios (id, conteudo, autor_nome, post_id, criado_em)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, conteudo, autor_nome, post_id, criado_em"#,
id,
input.conteudo,
input.autor_nome,
input.post_id,
agora
)
.fetch_one(db)
.await?;
Ok(comentario)
}
}
Servidor com Axum e Playground
use async_graphql::{dataloader::DataLoader, EmptySubscription, Schema};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
extract::State,
routing::get,
Router,
};
type BlogSchema = Schema<QueryRoot, MutationRoot, EmptySubscription>;
async fn graphql_handler(
State(schema): State<BlogSchema>,
req: GraphQLRequest,
) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
async fn graphql_playground() -> impl axum::response::IntoResponse {
axum::response::Html(
async_graphql::http::playground_source(
async_graphql::http::GraphQLPlaygroundConfig::new("/graphql")
)
)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
// Conectar ao banco de dados
let pool = sqlx::PgPool::connect("postgres://user:pass@localhost/blog")
.await?;
// Criar DataLoader
let autor_loader = DataLoader::new(
AutorLoader { pool: pool.clone() },
tokio::spawn,
);
// Criar schema GraphQL
let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription)
.data(pool)
.data(autor_loader)
.finish();
let app = Router::new()
.route("/graphql", get(graphql_playground).post(graphql_handler))
.with_state(schema);
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
tracing::info!("GraphQL playground em http://localhost:8080/graphql");
axum::serve(listener, app).await?;
Ok(())
}
Exemplo de Queries GraphQL
# Buscar posts com autores
query {
posts(limite: 10) {
id
titulo
conteudo
publicado
criadoEm
autor {
nome
email
totalPosts
}
comentarios {
conteudo
autorNome
}
}
}
# Criar novo post
mutation {
criarPost(input: {
titulo: "Introdução ao Rust"
conteudo: "Rust é uma linguagem..."
autorId: "550e8400-e29b-41d4-a716-446655440000"
publicado: true
}) {
id
titulo
}
}
# Buscar posts por texto
query {
buscarPosts(termo: "rust") {
titulo
autor {
nome
}
}
}
Performance: GraphQL em Rust vs Outras Linguagens
| Métrica | Rust (async-graphql) | Node.js (Apollo) | Python (Strawberry) |
|---|---|---|---|
| Req/s (query simples) | ~45.000 | ~8.000 | ~2.500 |
| Req/s (query complexa) | ~12.000 | ~2.000 | ~500 |
| Latência p99 | 2ms | 15ms | 45ms |
| Memória em idle | 5 MB | 80 MB | 50 MB |
| Memória sob carga | 25 MB | 300 MB | 200 MB |
Empresas Usando GraphQL com Rust
- Grafbase: Plataforma GraphQL serverless construída inteiramente em Rust
- Apollo (parcial): Router do Apollo Federation tem componentes em Rust
- Cloudflare: APIs internas GraphQL de alta performance
- Shopify: Componentes de performance para a API GraphQL
- GitHub: Experimenta Rust para componentes de alta performance na API GraphQL
Como Começar
- Aprenda Rust: Tutorial de primeiros passos — entenda ownership antes de criar APIs
- Domine REST primeiro: Siga o tutorial de API REST com Axum para entender o ecossistema web
- Async Rust: GraphQL é assíncrono por natureza — receita de async/await
- Banco de dados: Conecte ao PostgreSQL com SQLx
- JSON: Entenda serialização JSON com serde
- Comece com async-graphql: A documentação oficial e exemplos são excelentes
Conclusão
Rust combina perfeitamente com GraphQL. A validação de schema em tempo de compilação, a resolução assíncrona eficiente e o DataLoader integrado tornam async-graphql uma das melhores implementações GraphQL em qualquer linguagem. Se você já tem uma API REST em Rust e quer migrar para GraphQL, ou está começando um projeto novo que precisa de flexibilidade nas queries, Rust com async-graphql é uma escolha excelente.
Veja Também
- Tutorial: API REST com Axum — Comece com REST antes de migrar para GraphQL
- Rust para Desenvolvimento Web — Ecossistema web completo
- Rust para Microsserviços — Combine GraphQL com gRPC
- Tutorial: Rust + PostgreSQL — Banco de dados para sua API
- Receita: Serializar JSON — Essencial para APIs
- Receita: Async/Await Básico — Fundamento para GraphQL assíncrono