Почему Rust — идеальный кандидат для работы с API?
Если вы до сих пор думаете, что Rust — это только про системное программирование и замену C++, вы сильно отстали от реальности. Rust уверенно ворвался в мир веб-разработки и бэкенд-сервисов. И не просто ворвался, а занял нишу, где важны скорость, надёжность и безопасность памяти.
Но давайте честно: работать с API на Rust — это не просто «взять готовую библиотеку и забыть». Это искусство баланса между мощью языка и удобством разработки. В этой статье мы разберём три главных подхода: REST, GraphQL и gRPC. Каждый из них хорош по-своему, и выбор зависит от вашей задачи. Поехали!
REST на Rust: классика, которая работает
REST остаётся самым популярным стилем API. Он прост, интуитивен и поддерживается буквально всем. Для Rust есть несколько отличных фреймворков, но король здесь — actix-web. Он быстрый, асинхронный и с отличной экосистемой.
Пишем простой REST-сервис на actix-web
Давайте создадим мини-сервис для управления списком задач (todo). Нам понадобится actix-web, serde для сериализации и tokio как асинхронный рантайм.
// Cargo.toml
// [dependencies]
// actix-web = "4"
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
// tokio = { version = "1", features = ["full"] }
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Serialize, Deserialize, Clone)]
struct Task {
id: u32,
title: String,
completed: bool,
}
struct AppState {
tasks: Mutex<Vec<Task>>,
}
#[get("/tasks")]
async fn get_tasks(data: web::Data<AppState>) -> impl Responder {
let tasks = data.tasks.lock().unwrap();
HttpResponse::Ok().json(tasks.clone())
}
#[post("/tasks")]
async fn create_task(data: web::Data<AppState>, body: web::Json<Task>) -> impl Responder {
let mut tasks = data.tasks.lock().unwrap();
let new_id = tasks.len() as u32 + 1;
let task = Task {
id: new_id,
title: body.title.clone(),
completed: false,
};
tasks.push(task.clone());
HttpResponse::Created().json(task)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = web::Data::new(AppState {
tasks: Mutex::new(vec![]),
});
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.service(get_tasks)
.service(create_task)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Красота этого кода в том, что он безопасен по памяти и не требует сборщика мусора. actix-web обрабатывает тысячи запросов в секунду на одном потоке, используя асинхронные механизмы Rust. Попробуйте запустить — вы удивитесь, насколько он быстрый даже на слабом железе.
Когда REST — не лучший выбор?
REST отлично подходит для CRUD-операций, но если вам нужно гибко запрашивать данные (например, вложенные ресурсы), REST начинает требовать множества эндпоинтов. Тут на сцену выходит GraphQL.
GraphQL на Rust: гибкость без боли
GraphQL позволяет клиенту запрашивать ровно те данные, которые нужны. Для Rust лучший выбор — async-graphql. Он тесно интегрируется с actix-web и предоставляет мощный DSL для описания схем.
Пример: API для библиотеки книг
Допустим, мы хотим дать клиентам возможность запрашивать книги и авторов. С GraphQL это делается одной схемой.
// Cargo.toml
// [dependencies]
// async-graphql = "7"
// async-graphql-actix-web = "7"
// actix-web = "4"
// serde = { version = "1", features = ["derive"] }
use async_graphql::{Context, EmptyMutation, Object, Schema, SimpleObject};
use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse};
use actix_web::{web, App, HttpServer};
#[derive(SimpleObject)]
struct Book {
id: i32,
title: String,
author: String,
}
struct Query;
#[Object]
impl Query {
async fn books(&self, _ctx: &Context<'_>) -> Vec<Book> {
vec![
Book { id: 1, title: "Rust in Action".into(), author: "Tim McNamara".into() },
Book { id: 2, title: "Programming Rust".into(), author: "Jim Blandy".into() },
]
}
async fn book(&self, _ctx: &Context<'_>, id: i32) -> Option<Book> {
// Здесь можно сделать запрос к БД
if id == 1 {
Some(Book { id: 1, title: "Rust in Action".into(), author: "Tim McNamara".into() })
} else {
None
}
}
}
type AppSchema = Schema<Query, EmptyMutation, async_graphql::EmptySubscription>;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let schema = Schema::build(Query, EmptyMutation, async_graphql::EmptySubscription).finish();
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(schema.clone()))
.route("/graphql", web::post().to(graphql_handler))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
async fn graphql_handler(schema: web::Data<AppSchema>, req: GraphQLRequest) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
Этот код — настоящая находка для разработчиков, которым нужна гибкость запросов. Представьте: фронтенд может запросить только title и author, а не весь объект. Это уменьшает трафик и ускоряет загрузку.
Минусы GraphQL на Rust
GraphQL требует больше boilerplate-кода, чем REST. Кроме того, сложность схемы растёт с каждым новым типом. Но если вам важна гибкость — это лучший выбор.
gRPC на Rust: скорость на стероидах
Если вы работаете с микросервисами или высоконагруженными системами, gRPC — ваш выбор. Он использует Protocol Buffers (protobuf) и HTTP/2, что даёт в 5-10 раз большую производительность по сравнению с REST/JSON.
Для Rust есть tonic — современная библиотека, которая полностью реализует gRPC поверх tokio.
Создаём gRPC сервис для обработки заказов
Сначала определим protobuf-файл:
// proto/orders.proto
syntax = "proto3";
package orders;
service OrderService {
rpc GetOrder (OrderRequest) returns (OrderResponse);