Introduction to GraphQL API in Rust
Modern web development requires flexible and high-performance APIs. GraphQL is a query language that allows clients to get exactly the data they need without redundancy. Rust, in turn, offers high performance and memory safety. In this tutorial, we will create a simple but full-featured GraphQL server in Rust using the async-graphql library. You will learn how to set up a project, define data types, write resolvers, and run the server. This article is intended for beginner developers familiar with the basics of Rust.
Project Setup and Dependency Installation
First, let's create a new Rust project using Cargo:
cargo new graphql-rust-tutorialcd graphql-rust-tutorialOpen the Cargo.toml file and add the following dependencies:
[dependencies]async-graphql = "7.0"async-graphql-actix-web = "7.0"actix-web = "4"serde = { version = "1.0", features = ["derive"] }tokio = { version = "1", features = ["full"] }Explanation of library choices:
- async-graphql — the main library for working with GraphQL in Rust.
- async-graphql-actix-web — integration with the Actix-web web framework.
- actix-web — a popular asynchronous HTTP server.
- serde — for data serialization/deserialization.
- tokio — asynchronous runtime.
Run cargo build to ensure all dependencies are installed correctly.
Defining the GraphQL Schema and Resolvers
Let's create a simple data model — a book. We'll define the structure and resolvers for queries and mutations.
Creating the Data Model
Create a file src/models.rs and add the following code:
use async_graphql::*;
#[derive(SimpleObject)]pub struct Book { pub id: ID, pub title: String, pub author: String,}
#[derive(InputObject)]pub struct NewBook { pub title: String, pub author: String,}Here, SimpleObject automatically generates a GraphQL type for the Book struct, and InputObject does the same for mutation input data.
Creating Storage and Resolvers
Let's create a file src/resolvers.rs that will contain the query and mutation logic:
use std::sync::Mutex;use async_graphql::*;use crate::models::{Book, NewBook};
pub struct QueryRoot;
#[Object]impl QueryRoot { async fn books(&self) -> Vec<Book> { let storage = STORAGE.lock().unwrap(); storage.clone() }
async fn book(&self, id: ID) -> Option<Book> { let storage = STORAGE.lock().unwrap(); storage.iter().find(|b| b.id == id).cloned() }}
pub struct MutationRoot;
#[Object]impl MutationRoot { async fn add_book(&self, input: NewBook) -> Book { let mut storage = STORAGE.lock().unwrap(); let new_book = Book { id: ID::from(format!("book_{}", storage.len() + 1)), title: input.title, author: input.author, }; storage.push(new_book.clone()); new_book }}
lazy_static::lazy_static! { static ref STORAGE: Mutex<Vec<Book>> = Mutex::new(vec![]);}Note: we use Mutex for thread-safe access to storage. In a real project, it's better to use a database.
Building the Schema
Now let's combine everything in the src/main.rs file:
use actix_web::{web, App, HttpServer, HttpResponse, middleware};use async_graphql::{http::GraphiQLSource, EmptySubscription, Schema};use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse};use std::sync::Arc;
mod models;mod resol