Rust и API: от REST до gRPC — пишем быстро и безопасно

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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);                

Блоги

Рекомендаций по книгам