Введение: Почему 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
}