Как написать REST API на Rust за 30 минут: практическое руководство

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

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

Начать курс

Введение: Почему Rust для REST API?

Rust — это современный системный язык программирования, который сочетает в себе производительность C++ и безопасность памяти. В последние годы он активно используется для создания высоконагруженных веб-сервисов и REST API. В этом руководстве мы за 30 минут создадим полноценный REST API на Rust с использованием фреймворка Actix-web.

Почему стоит выбрать Rust для REST API?

  • Производительность: Rust компилируется в нативный код и не использует сборщик мусора, что даёт максимальную скорость обработки запросов.
  • Безопасность: Система владения (ownership) и заимствования (borrowing) исключает целые классы ошибок, связанных с памятью.
  • Масштабируемость: Actix-web — один из самых быстрых веб-фреймворков в мире, способный обрабатывать миллионы запросов в секунду.

К концу статьи у вас будет готовый проект с несколькими эндпоинтами, обработкой ошибок и структурой для дальнейшего расширения.

Шаг 1: Настройка проекта и установка зависимостей

Первым делом создадим новый проект Cargo. Откройте терминал и выполните:

cargo new rust_rest_api
cd rust_rest_api

Теперь откройте файл Cargo.toml и добавьте следующие зависимости:

[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
uuid = { version = "1.0", features = ["v4", "serde"] }

Давайте разберём, за что отвечает каждая библиотека:

  • actix-web — основной веб-фреймворк для создания HTTP-сервера.
  • serde / serde_json — сериализация и десериализация данных в формат JSON.
  • tokio — асинхронный рантайм, необходимый для работы Actix.
  • uuid — генерация уникальных идентификаторов для наших сущностей.

Выполните cargo build, чтобы загрузить все зависимости. Это займёт некоторое время при первом запуске.

Шаг 2: Создание модели данных и хранилища

Наше API будет управлять списком задач (TODO-приложение). Создадим простую модель. Откройте src/main.rs и добавьте следующий код:

use actix_web::{web, App, HttpServer, HttpResponse, middleware::Logger};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use uuid::Uuid;

// Модель задачи
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Task {
    id: Uuid,
    title: String,
    completed: bool,
}

// Запрос на создание задачи
#[derive(Debug, Deserialize)]
struct CreateTaskRequest {
    title: String,
}

// Хранилище задач (в памяти)
struct AppState {
    tasks: Mutex>,
}

Мы используем Mutex для потокобезопасного доступа к данным. В реальном проекте вы бы заменили это на базу данных, но для демонстрации подойдёт и такой вариант.

Теперь добавим функцию для инициализации состояния приложения:

fn init_tasks() -> Vec {
    vec![
        Task {
            id: Uuid::new_v4(),
            title: "Изучить Rust".to_string(),
            completed: false,
        },
        Task {
            id: Uuid::new_v4(),
            title: "Написать REST API".to_string(),
            completed: true,
        },
    ]
}

Шаг 3: Реализация CRUD-эндпоинтов

Теперь создадим основные обработчики для нашего REST API. Добавьте следующий код после объявления моделей:

GET /tasks — получение списка всех задач

async fn get_tasks(data: web::Data<AppState>) -> HttpResponse {
    let tasks = data.tasks.lock().unwrap();
    HttpResponse::Ok().json(tasks.clone())
}

POST /tasks — создание новой задачи

async fn create_task(
    data: web::Data<AppState>,
    body: web::Json<CreateTaskRequest>,
) -> HttpResponse {
    let mut tasks = data.tasks.lock().unwrap();
    let new_task = Task {
        id: Uuid::new_v4(),
        title: body.title.clone(),
        completed: false,
    };
    tasks.push(new_task.clone());
    HttpResponse::Created().json(new_task)
}

PUT /tasks/{id} — обновление задачи

async fn update_task(
    data: web::Data<AppState>,
    path: web::Path<Uuid>,
    body: web::Json<CreateTaskRequest>,
) -> HttpResponse {
    let mut tasks = data.tasks.lock().unwrap();
    if let Some(task) = tasks.iter_mut().find(|t| t.id == path.into_inner()) {
        task.title = body.title.clone();
        task.completed = !task.completed;
        HttpResponse::Ok().json(task.clone())
    } else {
        HttpResponse::NotFound().json("Задача не найдена")
    }
}

DELETE /tasks/{id} — удаление задачи

async fn delete_task(
    data: web::Data<AppState>,
    path: web::Path<Uuid>,
) -> HttpResponse {
    let mut tasks = data.tasks.lock().unwrap();
    let id = path.into_inner();
    let initial_len = tasks.len();
    tasks.retain(|t| t.id != id);
    
    if tasks.len() < initial_len {
        HttpResponse::Ok().json("Задача удалена")
    } else {
        HttpResponse::NotFound().json("Задача не найдена")
    }
}

Шаг 4: Сборка и запуск сервера

Осталось соединить все компоненты в функции main:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init();
    
    let app_state = web::Data::new(AppState {
        tasks: Mutex::new(init_tasks()),
    });

    println!("Сервер запущен на http://127.0.0.1:8080");

    HttpServer::new(move || {
        App::new()
            .app_data(app_state.clone())
            .wrap(Logger::default())
            .route("/tasks", web::get().to(get_tasks))
            .route("/tasks", web::post().to(create_task))
            .route("/tasks/{id}", web::put().to(update_task))
            .route("/tasks/{id}", web::delete().to(delete_task))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Блоги

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