Введение в GraphQL API на Rust
Современная веб-разработка требует высокой производительности и гибкости при работе с данными. Rust — язык системного программирования, известный своей скоростью и безопасностью памяти, а GraphQL — мощный язык запросов, позволяющий клиентам получать только необходимые данные. Объединение этих технологий даёт невероятно быстрые и удобные API.
В этом туториале мы создадим полноценный GraphQL API на Rust с использованием популярных крейтов Juniper (для реализации GraphQL) и Actix-web (для HTTP-сервера). Вы узнаете, как настроить проект, определить схему, реализовать резолверы и обработать запросы.
Статья рассчитана на разработчиков, уже знакомых с основами Rust и базовыми концепциями веб-API. Мы будем использовать Rust 2021 edition и последние стабильные версии зависимостей.
Шаг 1: Настройка проекта и зависимости
Начнём с создания нового проекта Rust. Откройте терминал и выполните:
cargo new graphql-rust-api
cd graphql-rust-api
Затем откройте файл Cargo.toml и добавьте следующие зависимости:
[dependencies]
actix-web = "4"
actix-rt = "2"
juniper = "0.16"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
- actix-web — асинхронный веб-фреймворк для Rust.
- juniper — библиотека для реализации GraphQL на Rust.
- serde — для сериализации/десериализации данных.
- tokio — асинхронный рантайм, необходимый для Actix.
После обновления зависимостей выполните cargo build, чтобы убедиться, что всё устанавливается корректно.
Шаг 2: Определение модели данных и GraphQL-схемы
Для примера создадим простую модель "Книга" с полями: id, название, автор и год издания. Создадим файл src/models.rs:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Book {
pub id: i32,
pub title: String,
pub author: String,
pub year: i32,
}
Теперь определим GraphQL-схему. Создадим src/graphql.rs:
use juniper::{FieldResult, GraphQLObject, GraphQLInputObject, RootNode};
use crate::models::Book;
// GraphQL-тип для книги (вывод)
#[derive(GraphQLObject)]
#[graphql(description = "Книга")]
pub struct BookGQL {
pub id: i32,
pub title: String,
pub author: String,
pub year: i32,
}
// Входной тип для создания книги
#[derive(GraphQLInputObject)]
#[graphql(description = "Входные данные для новой книги")]
pub struct NewBook {
pub title: String,
pub author: String,
pub year: i32,
}
// "База данных" в памяти
use std::sync::Mutex;
lazy_static::lazy_static! {
static ref BOOKS: Mutex<Vec<Book>> = Mutex::new(vec![
Book { id: 1, title: "Rust в действии".to_string(), author: "Тим Макнамара".to_string(), year: 2021 },
Book { id: 2, title: "Программирование на Rust".to_string(), author: "Джим Блэнди".to_string(), year: 2020 },
]);
}
// Корневой запрос (Query)
pub struct QueryRoot;
#[juniper::object]
impl QueryRoot {
fn books() -> FieldResult<Vec<BookGQL>> {
let books = BOOKS.lock().unwrap();
Ok(books.iter().map(|b| BookGQL {
id: b.id,
title: b.title.clone(),
author: b.author.clone(),
year: b.year,
}).collect())
}
fn book(id: i32) -> FieldResult<BookGQL> {
let books = BOOKS.lock().unwrap();
let book = books.iter().find(|b| b.id == id)
.ok_or_else(|| juniper::FieldError::new("Книга не найдена", graphql_value!({"code": "NOT_FOUND"})))?;
Ok(BookGQL {
id: book.id,
title: book.title.clone(),
author: book.author.clone(),
year: book.year,
})
}
}
// Корневая мутация (Mutation)
pub struct MutationRoot;
#[juniper::object]
impl MutationRoot {
fn create_book(new_book: NewBook) -> FieldResult<BookGQL> {
let mut books = BOOKS.lock().unwrap();
let new_id = books.len() as i32 + 1;
let book = Book {
id: new_id,
title: new_book.title,
author: new_book.author,
year: new_book.year,
};
books.push(book.clone());
Ok(BookGQL {
id: book.id,
title: book.title,
author: book.author,
year: book.year,
})
}
}
// Создаём корневой узел схемы
pub type Schema = RootNode<'static, QueryRoot, MutationRoot>;
pub fn create_schema() -> Schema {
Schema::new(QueryRoot {}, MutationRoot {})
}
Обратите внимание: мы используем lazy_static для глобального хранилища. Добавьте lazy_static = "1.4" в Cargo.toml.
Шаг 3: Настройка Actix-web сервера
Теперь свяжем GraphQL-схему с HTTP-сервером. Отредактируем src/main.rs:
mod models;
mod graphql;
use actix_web::{web, App, HttpServer, HttpResponse, middleware};
use juniper::http::graphiql::graphiql_source;
use juniper::http::GraphQLRequest;
use std::sync::Arc;
async fn graphql_handler(
schema: web::Data<Arc<graphql::Schema>>,
data: web::Json<GraphQLRequest>,
) -> HttpResponse {
let response = data.execute(&schema, &()).await;
HttpResponse::Ok().json(response)
}
async fn graphiql_handler() -> HttpResponse {
let html = graphiql_source("/graphql", None);
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(html)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let schema = Arc::new(graphql::create_schema());
println!("Сервер запущен на http://127.0.0.1:8080");
println!("GraphQL Playground: http://127.0.0.1:8080/graphiql");
HttpServer::new(move || {