GraphQL API with Rust: Step-by-Step Practical Tutorial
GraphQL is a powerful query language for APIs that allows clients to request only the data they need. Combined with Rust's performance and safety, you get an ideal solution for high-load web services. In this tutorial, we will build a full-fledged GraphQL API from scratch using the async-graphql crate.
Why Rust? It provides zero-cost abstractions, memory safety, and incredible speed. GraphQL in Rust runs faster than JavaScript or Python equivalents, which is critical for production systems.
1. Project Setup and Dependencies
Let's start by creating a new Rust project. Open your terminal and run:
cargo new graphql_rust_apicd graphql_rust_apiNow add the necessary dependencies to the Cargo.toml file:
[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"] }These crates provide everything you need: async-graphql — the GraphQL core, actix-web — the web server, serde — data serialization.
2. Creating the Data Model and Schema
Let's define a simple model for our API — books. Create the structures and GraphQL schema:
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,}Notice the #[derive(SimpleObject)] and #[derive(InputObject)] attributes. The first automatically generates a GraphQL type for output, the second for data input.
3. Implementing Resolvers (Query and Mutation)
Resolvers are functions that handle queries. Let's create a storage structure and 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 }}We use Mutex for thread-safe data access. In a real project, you would connect a database, but for this tutorial it's an ideal choice.
4. Building and Running the Server
The final step is to combine everything into a working server. Let's create the entry point:
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::