Введение: REST или GraphQL — вечный спор, который мы решим на Rust
Если вы пишете на Rust и выбираете архитектуру для своего API, то наверняка задавались вопросом: что выбрать — классический REST или модный GraphQL? Оба подхода имеют армию фанатов и гору хейтеров. REST обвиняют в избыточности данных и "болтовне" клиента с сервером. GraphQL — в сложности кэширования и "адовых" запросах к базе данных.
Но давайте посмотрим правде в глаза: Rust — это не JavaScript и не Python. Его философия — zero-cost abstractions и максимальная производительность. Значит ли это, что один из подходов лучше подходит для экосистемы Rust? Давайте разбираться на реальных примерах с кодом.
В этой статье мы не просто перечислим плюсы и минусы. Мы напишем два микросервиса на actix-web (REST) и async-graphql, сравним их производительность, сложность разработки и удобство поддержки. Поехали!
Краткий ликбез: REST и GraphQL за 30 секунд
REST (Representational State Transfer) — это архитектурный стиль, где каждый ресурс (пользователь, статья, заказ) имеет свой URL. Вы получаете данные через GET, создаёте через POST, обновляете через PUT/PATCH. Всё просто и предсказуемо. Но часто вы получаете больше данных, чем нужно, или делаете несколько запросов, чтобы собрать связанные сущности.
GraphQL — это язык запросов и среда выполнения. Клиент сам решает, какие поля ему нужны. Один запрос — и вы получаете ровно то, что попросили. Но за эту гибкость приходится платить сложностью реализации на сервере и потенциальными проблемами с производительностью.
В Rust оба подхода реализованы на высшем уровне благодаря системе типов и асинхронности. Но давайте копать глубже.
REST на Rust: классика с actix-web
Начнём с привычного REST. Для примера возьмём простой блог: есть пользователи, статьи и комментарии. В REST нам нужно будет создать несколько эндпоинтов.
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Article {
id: i32,
title: String,
content: String,
author_id: i32,
}
async fn get_article(path: web::Path<i32>) -> impl Responder {
let article_id = path.into_inner();
// Представим, что мы достаём статью из БД
let article = Article {
id: article_id,
title: "GraphQL vs REST".to_string(),
content: "Подробное сравнение...".to_string(),
author_id: 1,
};
HttpResponse::Ok().json(article)
}
async fn get_articles_by_author(path: web::Path<i32>) -> impl Responder {
let author_id = path.into_inner();
// Список статей автора
let articles = vec![
Article { id: 1, title: "Статья 1".to_string(), content: "Текст...".to_string(), author_id },
Article { id: 2, title: "Статья 2".to_string(), content: "Текст...".to_string(), author_id },
];
HttpResponse::Ok().json(articles)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/articles/{id}", web::get().to(get_article))
.route("/authors/{id}/articles", web::get().to(get_articles_by_author))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Всё выглядит чисто и понятно. Плюсы REST на Rust:
- Простота кэширования на уровне HTTP (ETag, Cache-Control).
- Прозрачная структура — каждый эндпоинт делает одну вещь.
- Лёгкая интеграция с любыми клиентами (curl, Postman, браузер).
Но есть и минусы:
- Проблема N+1 запросов. Чтобы получить статью и автора, нужно два запроса. Или создавать отдельный эндпоинт с JOIN.
- Over-fetching. Клиент получает все поля Article, даже если ему нужен только title.
- Со временем количество эндпоинтов растёт как снежный ком.
GraphQL на Rust: гибкость с async-graphql
Теперь реализуем ту же логику, но через GraphQL. Для Rust лучшая библиотека — async-graphql. Она отлично интегрируется с actix-web и tokio.
use async_graphql::*;
use async_graphql::http::GraphQLPlaygroundConfig;
use actix_web::{web, App, HttpServer, HttpResponse, middleware};
struct Query;
#[Object]
impl Query {
async fn article(&self, ctx: &Context<'_>, id: i32) -> Result {
// Получаем статью из БД
Ok(Article {
id,
title: "GraphQL vs REST".to_string(),
content: "Текст...".to_string(),
author_id: 1,
})
}
async fn articles_by_author(&self, ctx: &Context<'_>, author_id: i32) -> Result> {
Ok(vec![
Article { id: 1, title: "Статья 1".to_string(), content: "Текст...".to_string(), author_id },
Article { id: 2, title: "Статья 2".to_string(), content: "Текст...".to_string(), author_id },
])
}
}
#[derive(SimpleObject)]
struct Article {
id: i32,
title: String,
content: String,
#[graphql(skip)]
author_id: i32,
}
async fn index() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(GraphQLPlaygroundConfig::new("/graphql").title("Blog API").into_html())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
HttpServer::new(move || {
App::new()
.service(web::resource("/graphql").route(web::post().to(
|schema: web::Data<Schema<Query, EmptyMutation, EmptySubscription>>,
req: actix_web::HttpRequest,
payload: actix_web::web::Payload| {
async_graphql_actix_web::GraphQL::new(schema.get_ref().clone())
.respond(&req, payload)
},
)))
.service(web::resource("/playground").route(web::get().to(index)))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Теперь клиент может запросить только нужные поля:
query {
article(id: 1) {
title
author_id
}
}
Плюсы GraphQL на Rust:
- Клиент сам решает, какие данные получать. Никакого over-fetching.
- Один эндпоинт — вся логика в schema.
- Встроенная документация через GraphQL Playground.
Минусы:
- Сложное кэширование. POST-запросы не кэшируются так же легко, как GET.
- Проблема N