Neste projeto vamos construir uma API GraphQL completa em Rust usando a crate async-graphql integrada com Axum. A API vai gerenciar um catalogo de livros com queries para busca, mutations para criacao e atualizacao, e um playground interativo para testar operacoes. GraphQL oferece uma alternativa poderosa ao REST, permitindo que clientes solicitem exatamente os dados que precisam em uma unica requisicao.
Rust e uma excelente escolha para servidores GraphQL: o sistema de tipos forte se alinha perfeitamente com os schemas tipados do GraphQL, e a performance assincrona do tokio garante respostas rapidas mesmo sob carga elevada.
O Que Vamos Construir
Uma API GraphQL para catalogo de livros com:
- Queries – Listar livros, buscar por ID, filtrar por autor
- Mutations – Criar livro, atualizar livro, remover livro
- Schema tipado com objetos, inputs e enums
- Playground interativo no navegador para testar queries
- Armazenamento em memoria com estado compartilhado
- Tratamento de erros com mensagens descritivas
Estrutura do Projeto
graphql-api/
├── Cargo.toml
└── src/
└── main.rs
Configurando o Projeto
cargo new graphql-api
cd graphql-api
Configure o Cargo.toml:
[package]
name = "graphql-api"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
async-graphql = "7"
async-graphql-axum = "7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4"] }
A crate async-graphql e a implementacao GraphQL mais popular em Rust. A crate async-graphql-axum fornece a integracao com o framework Axum.
Passo 1: Definindo os Modelos de Dados
Vamos definir os tipos que compoem nosso schema GraphQL:
use async_graphql::{Enum, InputObject, Object, SimpleObject, ID};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
// Genero literario (enum GraphQL)
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Enum, PartialEq, Eq)]
pub enum Genero {
Ficcao,
NaoFiccao,
Romance,
FiccaoCientifica,
Fantasia,
Terror,
Biografia,
Tecnico,
}
// Modelo de livro (objeto GraphQL)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Livro {
pub id: String,
pub titulo: String,
pub autor: String,
pub genero: Genero,
pub ano_publicacao: i32,
pub paginas: i32,
pub sinopse: Option<String>,
pub avaliacao: Option<f64>,
}
// Implementacao dos campos GraphQL para Livro
#[Object]
impl Livro {
async fn id(&self) -> &str {
&self.id
}
async fn titulo(&self) -> &str {
&self.titulo
}
async fn autor(&self) -> &str {
&self.autor
}
async fn genero(&self) -> Genero {
self.genero
}
async fn ano_publicacao(&self) -> i32 {
self.ano_publicacao
}
async fn paginas(&self) -> i32 {
self.paginas
}
async fn sinopse(&self) -> &Option<String> {
&self.sinopse
}
async fn avaliacao(&self) -> &Option<f64> {
&self.avaliacao
}
// Campo calculado: classificacao por avaliacao
async fn classificacao(&self) -> &str {
match self.avaliacao {
Some(nota) if nota >= 4.5 => "Excelente",
Some(nota) if nota >= 3.5 => "Bom",
Some(nota) if nota >= 2.5 => "Regular",
Some(_) => "Fraco",
None => "Sem avaliacao",
}
}
}
// Input para criar um novo livro
#[derive(Debug, InputObject)]
pub struct NovoLivroInput {
pub titulo: String,
pub autor: String,
pub genero: Genero,
pub ano_publicacao: i32,
pub paginas: i32,
pub sinopse: Option<String>,
}
// Input para atualizar um livro
#[derive(Debug, InputObject)]
pub struct AtualizarLivroInput {
pub titulo: Option<String>,
pub autor: Option<String>,
pub genero: Option<Genero>,
pub ano_publicacao: Option<i32>,
pub paginas: Option<i32>,
pub sinopse: Option<String>,
pub avaliacao: Option<f64>,
}
// Estado da aplicacao
pub type BancoDados = Arc<Mutex<HashMap<String, Livro>>>;
A macro #[Object] transforma a struct em um tipo de objeto GraphQL. #[InputObject] marca tipos usados como parametros de entrada em mutations. #[Enum] define um enum do schema.
Passo 2: Implementando Queries
As queries permitem buscar dados do catalogo:
pub struct QueryRoot;
#[Object]
impl QueryRoot {
// Listar todos os livros
async fn livros(
&self,
ctx: &async_graphql::Context<'_>,
) -> Vec<Livro> {
let db = ctx.data_unchecked::<BancoDados>();
let livros = db.lock().unwrap();
livros.values().cloned().collect()
}
// Buscar livro por ID
async fn livro(
&self,
ctx: &async_graphql::Context<'_>,
id: String,
) -> Option<Livro> {
let db = ctx.data_unchecked::<BancoDados>();
let livros = db.lock().unwrap();
livros.get(&id).cloned()
}
// Buscar livros por autor
async fn livros_por_autor(
&self,
ctx: &async_graphql::Context<'_>,
autor: String,
) -> Vec<Livro> {
let db = ctx.data_unchecked::<BancoDados>();
let livros = db.lock().unwrap();
livros
.values()
.filter(|l| l.autor.to_lowercase().contains(&autor.to_lowercase()))
.cloned()
.collect()
}
// Buscar livros por genero
async fn livros_por_genero(
&self,
ctx: &async_graphql::Context<'_>,
genero: Genero,
) -> Vec<Livro> {
let db = ctx.data_unchecked::<BancoDados>();
let livros = db.lock().unwrap();
livros
.values()
.filter(|l| l.genero == genero)
.cloned()
.collect()
}
// Estatisticas do catalogo
async fn estatisticas(
&self,
ctx: &async_graphql::Context<'_>,
) -> Estatisticas {
let db = ctx.data_unchecked::<BancoDados>();
let livros = db.lock().unwrap();
let total = livros.len() as i32;
let media_paginas = if total > 0 {
livros.values().map(|l| l.paginas).sum::<i32>() as f64 / total as f64
} else {
0.0
};
Estatisticas {
total_livros: total,
media_paginas,
}
}
}
#[derive(SimpleObject)]
struct Estatisticas {
total_livros: i32,
media_paginas: f64,
}
O async_graphql::Context permite acessar dados compartilhados (como nosso banco em memoria) dentro dos resolvers. Cada campo e um resolver assincrono.
Passo 3: Implementando Mutations
As mutations permitem modificar dados:
pub struct MutationRoot;
#[Object]
impl MutationRoot {
// Criar novo livro
async fn criar_livro(
&self,
ctx: &async_graphql::Context<'_>,
input: NovoLivroInput,
) -> async_graphql::Result<Livro> {
// Validacoes
if input.titulo.trim().is_empty() {
return Err("Titulo nao pode ser vazio".into());
}
if input.paginas <= 0 {
return Err("Numero de paginas deve ser positivo".into());
}
let db = ctx.data_unchecked::<BancoDados>();
let mut livros = db.lock().unwrap();
let livro = Livro {
id: Uuid::new_v4().to_string(),
titulo: input.titulo,
autor: input.autor,
genero: input.genero,
ano_publicacao: input.ano_publicacao,
paginas: input.paginas,
sinopse: input.sinopse,
avaliacao: None,
};
livros.insert(livro.id.clone(), livro.clone());
Ok(livro)
}
// Atualizar livro existente
async fn atualizar_livro(
&self,
ctx: &async_graphql::Context<'_>,
id: String,
input: AtualizarLivroInput,
) -> async_graphql::Result<Livro> {
let db = ctx.data_unchecked::<BancoDados>();
let mut livros = db.lock().unwrap();
let livro = livros
.get_mut(&id)
.ok_or_else(|| async_graphql::Error::new("Livro nao encontrado"))?;
if let Some(titulo) = input.titulo {
livro.titulo = titulo;
}
if let Some(autor) = input.autor {
livro.autor = autor;
}
if let Some(genero) = input.genero {
livro.genero = genero;
}
if let Some(ano) = input.ano_publicacao {
livro.ano_publicacao = ano;
}
if let Some(paginas) = input.paginas {
livro.paginas = paginas;
}
if let Some(sinopse) = input.sinopse {
livro.sinopse = Some(sinopse);
}
if let Some(avaliacao) = input.avaliacao {
if !(0.0..=5.0).contains(&avaliacao) {
return Err("Avaliacao deve ser entre 0.0 e 5.0".into());
}
livro.avaliacao = Some(avaliacao);
}
Ok(livro.clone())
}
// Remover livro
async fn remover_livro(
&self,
ctx: &async_graphql::Context<'_>,
id: String,
) -> async_graphql::Result<bool> {
let db = ctx.data_unchecked::<BancoDados>();
let mut livros = db.lock().unwrap();
if livros.remove(&id).is_some() {
Ok(true)
} else {
Err("Livro nao encontrado".into())
}
}
}
As mutations retornam async_graphql::Result para comunicar erros de forma idiomatica no GraphQL. As validacoes garantem dados consistentes.
Passo 4: Montando o main.rs Completo
Aqui esta o codigo completo do src/main.rs:
use async_graphql::{
Enum, EmptySubscription, InputObject, Object, Schema, SimpleObject,
};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
extract::State,
response::{Html, IntoResponse},
routing::get,
Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
// === Modelos ===
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Enum, PartialEq, Eq)]
pub enum Genero {
Ficcao,
NaoFiccao,
Romance,
FiccaoCientifica,
Fantasia,
Terror,
Biografia,
Tecnico,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Livro {
pub id: String,
pub titulo: String,
pub autor: String,
pub genero: Genero,
pub ano_publicacao: i32,
pub paginas: i32,
pub sinopse: Option<String>,
pub avaliacao: Option<f64>,
}
#[Object]
impl Livro {
async fn id(&self) -> &str { &self.id }
async fn titulo(&self) -> &str { &self.titulo }
async fn autor(&self) -> &str { &self.autor }
async fn genero(&self) -> Genero { self.genero }
async fn ano_publicacao(&self) -> i32 { self.ano_publicacao }
async fn paginas(&self) -> i32 { self.paginas }
async fn sinopse(&self) -> &Option<String> { &self.sinopse }
async fn avaliacao(&self) -> &Option<f64> { &self.avaliacao }
async fn classificacao(&self) -> &str {
match self.avaliacao {
Some(nota) if nota >= 4.5 => "Excelente",
Some(nota) if nota >= 3.5 => "Bom",
Some(nota) if nota >= 2.5 => "Regular",
Some(_) => "Fraco",
None => "Sem avaliacao",
}
}
}
#[derive(Debug, InputObject)]
pub struct NovoLivroInput {
pub titulo: String,
pub autor: String,
pub genero: Genero,
pub ano_publicacao: i32,
pub paginas: i32,
pub sinopse: Option<String>,
}
#[derive(Debug, InputObject)]
pub struct AtualizarLivroInput {
pub titulo: Option<String>,
pub autor: Option<String>,
pub genero: Option<Genero>,
pub ano_publicacao: Option<i32>,
pub paginas: Option<i32>,
pub sinopse: Option<String>,
pub avaliacao: Option<f64>,
}
#[derive(SimpleObject)]
struct Estatisticas {
total_livros: i32,
media_paginas: f64,
}
pub type BancoDados = Arc<Mutex<HashMap<String, Livro>>>;
// === Query ===
pub struct QueryRoot;
#[Object]
impl QueryRoot {
async fn livros(&self, ctx: &async_graphql::Context<'_>) -> Vec<Livro> {
let db = ctx.data_unchecked::<BancoDados>();
db.lock().unwrap().values().cloned().collect()
}
async fn livro(&self, ctx: &async_graphql::Context<'_>, id: String) -> Option<Livro> {
let db = ctx.data_unchecked::<BancoDados>();
db.lock().unwrap().get(&id).cloned()
}
async fn livros_por_autor(&self, ctx: &async_graphql::Context<'_>, autor: String) -> Vec<Livro> {
let db = ctx.data_unchecked::<BancoDados>();
db.lock().unwrap()
.values()
.filter(|l| l.autor.to_lowercase().contains(&autor.to_lowercase()))
.cloned()
.collect()
}
async fn livros_por_genero(&self, ctx: &async_graphql::Context<'_>, genero: Genero) -> Vec<Livro> {
let db = ctx.data_unchecked::<BancoDados>();
db.lock().unwrap()
.values()
.filter(|l| l.genero == genero)
.cloned()
.collect()
}
async fn estatisticas(&self, ctx: &async_graphql::Context<'_>) -> Estatisticas {
let db = ctx.data_unchecked::<BancoDados>();
let livros = db.lock().unwrap();
let total = livros.len() as i32;
let media = if total > 0 {
livros.values().map(|l| l.paginas).sum::<i32>() as f64 / total as f64
} else {
0.0
};
Estatisticas { total_livros: total, media_paginas: media }
}
}
// === Mutation ===
pub struct MutationRoot;
#[Object]
impl MutationRoot {
async fn criar_livro(
&self,
ctx: &async_graphql::Context<'_>,
input: NovoLivroInput,
) -> async_graphql::Result<Livro> {
if input.titulo.trim().is_empty() {
return Err("Titulo nao pode ser vazio".into());
}
if input.paginas <= 0 {
return Err("Numero de paginas deve ser positivo".into());
}
let db = ctx.data_unchecked::<BancoDados>();
let mut livros = db.lock().unwrap();
let livro = Livro {
id: Uuid::new_v4().to_string(),
titulo: input.titulo,
autor: input.autor,
genero: input.genero,
ano_publicacao: input.ano_publicacao,
paginas: input.paginas,
sinopse: input.sinopse,
avaliacao: None,
};
livros.insert(livro.id.clone(), livro.clone());
Ok(livro)
}
async fn atualizar_livro(
&self,
ctx: &async_graphql::Context<'_>,
id: String,
input: AtualizarLivroInput,
) -> async_graphql::Result<Livro> {
let db = ctx.data_unchecked::<BancoDados>();
let mut livros = db.lock().unwrap();
let livro = livros.get_mut(&id)
.ok_or_else(|| async_graphql::Error::new("Livro nao encontrado"))?;
if let Some(titulo) = input.titulo { livro.titulo = titulo; }
if let Some(autor) = input.autor { livro.autor = autor; }
if let Some(genero) = input.genero { livro.genero = genero; }
if let Some(ano) = input.ano_publicacao { livro.ano_publicacao = ano; }
if let Some(paginas) = input.paginas { livro.paginas = paginas; }
if let Some(sinopse) = input.sinopse { livro.sinopse = Some(sinopse); }
if let Some(avaliacao) = input.avaliacao {
if !(0.0..=5.0).contains(&avaliacao) {
return Err("Avaliacao deve ser entre 0.0 e 5.0".into());
}
livro.avaliacao = Some(avaliacao);
}
Ok(livro.clone())
}
async fn remover_livro(
&self,
ctx: &async_graphql::Context<'_>,
id: String,
) -> async_graphql::Result<bool> {
let db = ctx.data_unchecked::<BancoDados>();
if db.lock().unwrap().remove(&id).is_some() {
Ok(true)
} else {
Err("Livro nao encontrado".into())
}
}
}
// === Schema e Handlers ===
type AppSchema = Schema<QueryRoot, MutationRoot, EmptySubscription>;
async fn graphql_handler(
State(schema): State<AppSchema>,
req: GraphQLRequest,
) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
async fn graphql_playground() -> impl IntoResponse {
Html(async_graphql::http::playground_source(
async_graphql::http::GraphQLPlaygroundConfig::new("/graphql"),
))
}
// === Main ===
fn dados_iniciais() -> HashMap<String, Livro> {
let mut livros = HashMap::new();
let exemplos = vec![
Livro {
id: Uuid::new_v4().to_string(),
titulo: "O Senhor dos Aneis".to_string(),
autor: "J.R.R. Tolkien".to_string(),
genero: Genero::Fantasia,
ano_publicacao: 1954,
paginas: 1178,
sinopse: Some("Uma epica jornada pela Terra Media".to_string()),
avaliacao: Some(4.9),
},
Livro {
id: Uuid::new_v4().to_string(),
titulo: "O Programador Pragmatico".to_string(),
autor: "David Thomas e Andrew Hunt".to_string(),
genero: Genero::Tecnico,
ano_publicacao: 1999,
paginas: 352,
sinopse: Some("Guia pratico para desenvolvimento de software".to_string()),
avaliacao: Some(4.7),
},
Livro {
id: Uuid::new_v4().to_string(),
titulo: "Neuromancer".to_string(),
autor: "William Gibson".to_string(),
genero: Genero::FiccaoCientifica,
ano_publicacao: 1984,
paginas: 271,
sinopse: Some("O classico cyberpunk que definiu um genero".to_string()),
avaliacao: Some(4.2),
},
];
for livro in exemplos {
livros.insert(livro.id.clone(), livro);
}
livros
}
#[tokio::main]
async fn main() {
let db: BancoDados = Arc::new(Mutex::new(dados_iniciais()));
let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription)
.data(db)
.finish();
let app = Router::new()
.route("/graphql", get(graphql_playground).post(graphql_handler))
.with_state(schema);
let endereco = "0.0.0.0:3000";
println!("API GraphQL rodando em http://{}", endereco);
println!("Playground: http://{}/graphql", endereco);
let listener = tokio::net::TcpListener::bind(endereco).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Como Executar
Compile e inicie o servidor:
cargo run
Acesse o playground GraphQL no navegador em http://localhost:3000/graphql. Experimente as seguintes queries:
# Listar todos os livros
query {
livros {
id
titulo
autor
genero
classificacao
}
}
# Buscar livros por genero
query {
livrosPorGenero(genero: FANTASIA) {
titulo
autor
avaliacao
}
}
# Criar um novo livro
mutation {
criarLivro(input: {
titulo: "Duna"
autor: "Frank Herbert"
genero: FICCAO_CIENTIFICA
anoPublicacao: 1965
paginas: 412
sinopse: "Epopeia de ficcao cientifica no planeta deserto"
}) {
id
titulo
}
}
# Atualizar avaliacao
mutation {
atualizarLivro(id: "ID_DO_LIVRO", input: { avaliacao: 4.8 }) {
titulo
avaliacao
classificacao
}
}
# Ver estatisticas
query {
estatisticas {
totalLivros
mediaPaginas
}
}
Tambem pode usar curl:
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ livros { titulo autor } }"}'
Desafios para Expandir
Subscriptions em tempo real – Adicione subscriptions GraphQL para que clientes recebam notificacoes quando novos livros sao adicionados, usando
async-graphqlcom WebSockets.Paginacao e ordenacao – Implemente paginacao estilo Relay (cursors) e parametros de ordenacao (por titulo, data, avaliacao) na query de listagem de livros.
Relacionamentos – Adicione entidades
AutoreEditoracom relacionamentos N:N, permitindo queries aninhadas comolivro { autor { outrosLivros } }.DataLoader – Implemente batching com
async-graphql::dataloader::DataLoaderpara evitar o problema N+1 quando resolver relacionamentos.Autenticacao – Adicione autenticacao via header JWT no contexto GraphQL, com diretivas que restringem certas mutations a usuarios autenticados.
Veja Tambem
- Vec na Biblioteca Padrao – Colecoes usadas nos resultados de queries
- Option para Valores Opcionais – Campos opcionais no schema GraphQL
- Rust para GraphQL – Panorama do ecossistema GraphQL em Rust
- Rust para Desenvolvimento Web – Contexto geral de web em Rust
- Serializando JSON em Rust – Fundamentos de serializacao