Введение: Почему Rust для REST API?
Rust — это современный системный язык программирования, который сочетает в себе производительность C++ и безопасность памяти, гарантированную на этапе компиляции. В последние годы Rust активно используется для создания высоконагруженных веб-сервисов и микросервисов. В этой статье мы покажем, как за 30 минут написать полноценное REST API на Rust с помощью фреймворка Actix-web и библиотеки Serde.
Почему стоит выбрать Rust для вашего API?
- Производительность: Rust работает на уровне C/C++, что критично для высоконагруженных систем.
- Безопасность: Система владения (ownership) и заимствования (borrowing) исключает целые классы ошибок, связанных с памятью.
- Асинхронность: Встроенная поддержка async/await позволяет эффективно обрабатывать тысячи соединений.
- Экосистема: Cargo (пакетный менеджер) и crates.io делают управление зависимостями простым и удобным.
Мы создадим простое API для управления списком задач (Todo list), которое будет поддерживать CRUD-операции. Вы научитесь настраивать маршруты, обрабатывать JSON и работать с состоянием приложения.
Шаг 1: Настройка проекта и установка зависимостей
Первым делом создадим новый проект на Rust с помощью Cargo. Откройте терминал и выполните:
cargo new rust_todo_api
cd rust_todo_api
Теперь отредактируем файл Cargo.toml, добавив необходимые зависимости:
[package]
name = "rust_todo_api"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
uuid = { version = "1", features = ["v4"] }
Разберем, зачем нужна каждая библиотека:
- actix-web — производительный асинхронный веб-фреймворк.
- serde — библиотека для сериализации и десериализации данных (JSON).
- serde_json — поддержка формата JSON для serde.
- tokio — асинхронный рантайм для Rust (actix-web использует его по умолчанию).
- uuid — генерация уникальных идентификаторов для наших задач.
После добавления зависимостей выполните cargo build, чтобы скачать и скомпилировать все пакеты. Это может занять несколько минут при первом запуске.
Шаг 2: Создание модели данных и структуры приложения
Создадим файл src/main.rs и определим модель нашей задачи. Мы будем использовать структуру TodoItem с полями id, title и completed:
use actix_web::{web, App, HttpServer, HttpResponse, middleware};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone)]
struct TodoItem {
id: String,
title: String,
completed: bool,
}
#[derive(Debug, Deserialize)]
struct CreateTodoRequest {
title: String,
}
#[derive(Debug, Deserialize)]
struct UpdateTodoRequest {
title: Option<String>,
completed: Option<bool>,
}
struct AppState {
todos: Mutex<Vec<TodoItem>>,
}
Обратите внимание на важные детали:
SerializeиDeserialize— трейты из serde, которые автоматически генерируют код для работы с JSON.Mutex— используется для безопасного доступа к общему состоянию из нескольких потоков.Uuid— генерирует уникальные идентификаторы для каждой задачи.
Теперь добавим функцию main, которая будет запускать сервер и инициализировать состояние:
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let app_state = web::Data::new(AppState {
todos: Mutex::new(Vec::new()),
});
println!("Сервер запущен на http://127.0.0.1:8080");
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.route("/todos", web::get().to(get_todos))
.route("/todos", web::post().to(create_todo))
.route("/todos/{id}", web::get().to(get_todo_by_id))
.route("/todos/{id}", web::put().to(update_todo))
.route("/todos/{id}", web::delete().to(delete_todo))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Мы определили 5 маршрутов для полного CRUD. Теперь реализуем каждый обработчик.
Шаг 3: Реализация CRUD-обработчиков
Добавим функции-обработчики после структуры AppState. Начнем с получения всех задач:
async fn get_todos(data: web::Data<AppState>) -> HttpResponse {
let todos = data.todos.lock().unwrap();
HttpResponse::Ok().json(&*todos)
}
async fn create_todo(
data: web::Data<AppState>,
body: web::Json<CreateTodoRequest>,
) -> HttpResponse {
let mut todos = data.todos.lock().unwrap();
let new_todo = TodoItem {
id: Uuid::new_v4().to_string(),
title: body.title.clone(),
completed: false,
};
todos.push(new_todo.clone());
HttpResponse::Created().json(&new_todo)
}
async fn get_todo_by_id(
data: web::Data<AppState>,
path: web::Path<String>,
) -> HttpResponse {
let id = path.into_inner();
let todos = data.todos.lock().unwrap();
match todos.iter().find(|todo| todo.id == id) {
Some(todo) => HttpResponse::Ok().json(todo),
None => HttpResponse::NotFound().json({"error": "Todo not found"}),
}
}
async fn update_todo(
data: web::Data<AppState>,
path: web::Path<String>,
body: web::Json<UpdateTodoRequest>,
) -> HttpResponse {
let id = path.into_inner();
let mut todos = data.todos.lock().unwrap();
if let Some(todo) = todos.iter_mut().find(|todo| todo