API GraphQL con Rust: tutorial práctico paso a paso
GraphQL es un potente lenguaje de consultas para API que permite a los clientes solicitar solo los datos que necesitan. Combinado con el rendimiento y la seguridad de Rust, obtienes una solución ideal para servicios web de alta carga. En este tutorial crearemos una API GraphQL completa desde cero, utilizando el crate async-graphql.
¿Por qué Rust? Proporciona abstracciones de costo cero, seguridad de memoria y una velocidad impresionante. GraphQL en Rust funciona más rápido que sus equivalentes en JavaScript o Python, lo cual es crítico para sistemas en producción.
1. Configuración del proyecto y dependencias
Empecemos creando un nuevo proyecto Rust. Abre la terminal y ejecuta:
cargo new graphql_rust_apicd graphql_rust_apiAhora agreguemos las dependencias necesarias en el archivo Cargo.toml:
[dependencies]async-graphql = "7.0"async-graphql-actix-web = "7.0"actix-web = "4"actix-cors = "0.7"serde = { version = "1.0", features = ["derive"] }tokio = { version = "1", features = ["full"] }Estos crates proporcionan todo lo necesario: async-graphql — el núcleo de GraphQL, actix-web — el servidor web, serde — la serialización de datos.
2. Creación del modelo de datos y esquema
Definamos un modelo simple para nuestra API — libros. Crearemos las estructuras y el esquema GraphQL:
use async_graphql::*;use serde::{Serialize, Deserialize};
#[derive(Clone, Debug, Serialize, Deserialize, SimpleObject)]struct Book { id: ID, title: String, author: String, year: i32,}
#[derive(Clone, Debug, Serialize, Deserialize, InputObject)]struct BookInput { title: String, author: String, year: i32,}Observa los atributos #[derive(SimpleObject)] y #[derive(InputObject)]. El primero genera automáticamente un tipo GraphQL para salida, el segundo para entrada de datos.
3. Implementación de resolvers (Query y Mutation)
Los resolvers son funciones que procesan las consultas. Crearemos una estructura de almacenamiento y los resolvers:
use std::sync::Mutex;
struct QueryRoot;
#[Object]impl QueryRoot { async fn books(&self, ctx: &Context<'_>) -> Vec<Book> { let storage = ctx.data_unchecked::<Mutex<Vec<Book>>>(); storage.lock().unwrap().clone() }
async fn book(&self, ctx: &Context<'_>, id: ID) -> Option<Book> { let storage = ctx.data_unchecked::<Mutex<Vec<Book>>>(); storage.lock().unwrap().iter().find(|b| b.id == id).cloned() }}
struct MutationRoot;
#[Object]impl MutationRoot { async fn add_book(&self, ctx: &Context<'_>, input: BookInput) -> Book { let mut storage = ctx.data_unchecked::<Mutex<Vec<Book>>>() .lock().unwrap(); let book = Book { id: ID::from(storage.len() + 1), title: input.title, author: input.author, year: input.year, }; storage.push(book.clone()); book }}Usamos Mutex para acceso seguro a los datos en entornos concurrentes. En un proyecto real conectarías una base de datos, pero para este tutorial es la opción ideal.
4. Compilación e inicio del servidor
El paso final es unir todo en un servidor funcional. Crearemos el punto de entrada:
use actix_web::{guard, web, App, HttpServer, HttpRequest, HttpResponse};use actix_cors::Cors;use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse};
async fn index( schema: web::Data<Schema<QueryRoot, MutationRoot, EmptySubscription>>, req: GraphQLRequest,) -> GraphQLResponse { schema.execute(req.into_inner()).await.into()}
#[actix_web::main]async fn main() -> std::