Маршрутизация
Маршрутизация — основополагающий элемент любого веб-фреймворка, и Axum предлагает гибкую и мощную систему маршрутизации через тип Router
. В этом разделе мы рассмотрим все аспекты работы с маршрутами в Axum: от простых сопоставлений до сложной вложенной структуры.
Основы маршрутизации
Структура Router
Router
— центральный тип для маршрутизации в Axum. Он сопоставляет входящие HTTP-запросы с соответствующими обработчиками на основе пути и HTTP-метода.
use axum::{
Router,
routing::{get, post},
};
// Создание нового роутера
let app = Router::new()
.route("/", get(root))
.route("/users", get(list_users).post(create_user));
Router
использует строительный паттерн (builder pattern), где каждый метод, такой как .route()
, возвращает новый экземпляр Router. Это обеспечивает неизменяемость и позволяет создавать сложные маршруты в декларативном стиле.
Определение маршрутов
Метод .route()
принимает два аргумента:
- Путь (строка) — шаблон URL для сопоставления
- Обработчик или композицию обработчиков для различных HTTP-методов
// Сопоставление GET запросов к /hello с обработчиком hello_handler
router.route("/hello", get(hello_handler));
// Сопоставление нескольких HTTP-методов с разными обработчиками
router.route("/users",
get(list_users)
.post(create_user)
.put(update_user)
.delete(delete_user)
);
Функции HTTP-методов
Axum предоставляет удобные функции для каждого HTTP-метода:
use axum::routing::{get, post, put, delete, patch, head, options, trace, connect};
let router = Router::new()
.route("/", get(root))
.route("/resource",
get(get_resource)
.post(create_resource)
.put(replace_resource)
.patch(update_resource)
.delete(delete_resource)
)
.route("/status", head(status))
.route("/options", options(options_handler))
.route("/trace", trace(trace_handler))
.route("/connect", connect(connect_handler));
Каждая из этих функций принимает обработчик и возвращает типизированное значение, которое может быть использовано с методом .route()
.
Обработка всех HTTP-методов
Если вам нужно перехватить запросы со всеми HTTP-методами по определенному пути, используйте метод any
:
use axum::routing::any;
let router = Router::new()
.route("/any-method", any(handle_any));
async fn handle_any(
method: axum::http::Method,
uri: axum::http::Uri,
) -> String {
format!("Получен запрос: {} {}", method, uri)
}
Параметры пути
Базовый синтаксис
Axum поддерживает динамические сегменты в путях, обозначаемые двоеточием (:
) перед именем параметра:
let router = Router::new()
.route("/users/:user_id", get(get_user))
.route("/posts/:post_id/comments/:comment_id", get(get_comment));
async fn get_user(Path(user_id): Path<String>) -> String {
format!("Получен пользователь с ID: {}", user_id)
}
async fn get_comment(
Path((post_id, comment_id)): Path<(String, String)>
) -> String {
format!("Получен комментарий {} для поста {}", comment_id, post_id)
}
Типизированные параметры пути
По умолчанию параметры пути извлекаются как String
, но вы можете использовать специальные типы для автоматического преобразования и валидации:
use axum::extract::Path;
use uuid::Uuid;
use serde::Deserialize;
// Структура для параметров пути
#[derive(Deserialize)]
struct UserParams {
user_id: u64,
action: String,
}
// Использование Uuid для типобезопасной работы с ID
async fn get_post(Path(post_id): Path<Uuid>) -> String {
format!("Получен пост с UUID: {}", post_id)
}
// Извлечение нескольких параметров пути в структуру
async fn user_action(Path(params): Path<UserParams>) -> String {
format!("Действие {} для пользователя {}", params.action, params.user_id)
}
let router = Router::new()
.route("/posts/:post_id", get(get_post))
.route("/users/:user_id/:action", get(user_action));
При использовании типизированных параметров Axum автоматически вернет ошибку 404, если параметры не могут быть преобразованы в указанный тип.
Подстановочные знаки и захват остатка пути
Для захвата остатка пути можно использовать подстановочный знак *
:
let router = Router::new()
.route("/files/*path", get(serve_file));
async fn serve_file(Path(path): Path<String>) -> String {
format!("Запрошен файл по пути: {}", path)
}
Это полезно для реализации обработчиков файлов или прокси-серверов.
Композиция и вложенность маршрутов
Вложенные маршруты с методом nest
Метод .nest()
позволяет создавать иерархическую структуру маршрутов:
// Маршруты для API v1
let api_v1 = Router::new()
.route("/users", get(list_users_v1))
.route("/posts", get(list_posts_v1));
// Маршруты для API v2
let api_v2 = Router::new()
.route("/users", get(list_users_v2))
.route("/posts", get(list_posts_v2));
// Маршруты для аутентификации
let auth = Router::new()
.route("/login", post(login))
.route("/logout", post(logout))
.route("/register", post(register));
// Объединение всех маршрутов в иерархию
let app = Router::new()
.nest("/api/v1", api_v1)
.nest("/api/v2", api_v2)
.nest("/auth", auth)
.route("/", get(index));
В этом примере:
/api/v1/users
будет обрабатыватьсяlist_users_v1
/api/v2/users
будет обрабатыватьсяlist_users_v2
/auth/login
будет обрабатыватьсяlogin
Объединение маршрутов с методом merge
Метод .merge()
позволяет объединить два роутера в один на одном уровне:
// Маршруты для аутентифицированных пользователей
let protected = Router::new()
.route("/dashboard", get(dashboard))
.route("/settings", get(settings))
.layer(middleware::require_auth());
// Маршруты, доступные всем
let public = Router::new()
.route("/", get(index))
.route("/about", get(about))
.route("/login", post(login));
// Объединение публичных и защищенных маршрутов
let app = public.merge(protected);
Важное отличие от .nest()
заключается в том, что .merge()
объединяет маршруты на одном уровне иерархии, не добавляя префикса пути.
Обработка 404 ошибок
Служебный обработчик fallback
Метод .fallback()
позволяет указать обработчик для запросов, которые не соответствуют ни одному определенному маршруту:
let app = Router::new()
.route("/", get(index))
.route("/about", get(about))
.fallback(not_found);
async fn not_found() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "Страница не найдена")
}
Это создает "перехватчик" для всех необработанных запросов, что позволяет вернуть пользовательское сообщение об ошибке вместо стандартного 404.
Группировка маршрутов по функционалу
Для больших приложений часто полезно группировать маршруты по функциональным модулям:
// В файле routes/users.rs
pub fn user_routes() -> Router {
Router::new()
.route("/", get(list_users).post(create_user))
.route("/:id", get(get_user).patch(update_user).delete(delete_user))
}
// В файле routes/posts.rs
pub fn post_routes() -> Router {
Router::new()
.route("/", get(list_posts).post(create_post))
.route("/:id", get(get_post).patch(update_post).delete(delete_post))
.route("/:id/comments", get(list_comments).post(add_comment))
}
// В файле main.rs
let app = Router::new()
.nest("/users", user_routes())
.nest("/posts", post_routes())
.route("/", get(index));
Такой подход улучшает модульность и делает код более поддерживаемым.
Применение middleware к группам маршрутов
Вы можете применять middleware к определенным группам маршрутов, используя метод .layer()
:
use tower_http::trace::TraceLayer;
use tower_http::compression::CompressionLayer;
// Маршруты API с трассировкой и сжатием
let api = Router::new()
.route("/users", get(list_users))
.route("/posts", get(list_posts))
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new());
// Маршруты для статических файлов только со сжатием
let static_files = Router::new()
.route("/", get(serve_index))
.route("/*path", get(serve_static))
.layer(CompressionLayer::new());
// Объединение всех маршрутов
let app = Router::new()
.nest("/api", api)
.nest("/static", static_files);
Все middleware, применяемые к роутеру, будут выполняться для каждого маршрута в этом роутере.
Версионирование API
Axum позволяет легко реализовать версионирование API, используя вложенные маршруты:
// API версии 1
let v1 = Router::new()
.route("/users", get(list_users_v1))
.route("/posts", get(list_posts_v1));
// API версии 2
let v2 = Router::new()
.route("/users", get(list_users_v2))
.route("/posts", get(list_posts_v2))
.route("/comments", get(list_comments_v2)); // Новый ресурс в v2
let app = Router::new()
.nest("/api/v1", v1)
.nest("/api/v2", v2);
Также можно использовать HTTP-заголовки для версионирования, обрабатывая их в middleware.
Использование переменных окружения для управления маршрутами
Зачастую возникает необходимость включать или отключать определенные маршруты в зависимости от окружения (разработка, тестирование, продакшн):
use std::env;
let mut app = Router::new()
.route("/", get(index))
.route("/users", get(list_users));
// Добавление отладочных маршрутов только в dev-режиме
if env::var("APP_ENV").unwrap_or_default() == "development" {
app = app
.route("/debug/state", get(show_state))
.route("/debug/config", get(show_config));
}
Динамическое создание маршрутов
Иногда необходимо создавать маршруты динамически, например, на основе данных из конфигурационного файла:
use std::collections::HashMap;
// Допустим, у нас есть конфигурация маршрутов
let route_config: HashMap<String, String> = load_routes_from_config();
// Создаем базовый роутер
let mut app = Router::new();
// Динамически добавляем маршруты
for (path, resource) in route_config {
app = app.route(&path, get(move |Path(param): Path<String>| async move {
format!("Ресурс: {}, параметр: {}", resource, param)
}));
}
Этот подход пригодится для создания универсальных приложений, где маршрутизация задается через конфигурацию.
Параметры запроса (Query Parameters)
Хотя обработка параметров запроса относится к экстракторам (о которых речь пойдет в одном из следующих разделов), стоит упомянуть, что в контексте маршрутизации они не влияют на выбор обработчика:
use axum::extract::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct Params {
sort: Option<String>,
filter: Option<String>,
page: Option<u32>,
}
async fn list_items(Query(params): Query<Params>) -> String {
format!(
"Сортировка: {}, Фильтр: {}, Страница: {}",
params.sort.unwrap_or_default(),
params.filter.unwrap_or_default(),
params.page.unwrap_or(1)
)
}
let app = Router::new()
.route("/items", get(list_items));
Здесь маршрут /items
будет соответствовать запросам с любыми параметрами запроса, например /items?sort=name&page=2
.
Паттерны маршрутизации
RESTful маршруты
Axum хорошо подходит для создания RESTful API:
let api = Router::new()
// Ресурс User
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user))
// Вложенный ресурс Comment
.route("/users/:user_id/comments", get(list_user_comments).post(create_comment))
.route("/users/:user_id/comments/:comment_id", get(get_comment).put(update_comment).delete(delete_comment));
Обработка сложных URL
Для сложных URL с множеством параметров можно комбинировать параметры пути и запроса:
async fn complex_handler(
Path((category, subcategory)): Path<(String, String)>,
Query(params): Query<FilterParams>,
) -> impl IntoResponse {
// Обработка сложного запроса
}
let app = Router::new()
.route("/:category/:subcategory", get(complex_handler));
Лучшие практики маршрутизации
Структурирование маршрутов
- Группируйте связанные маршруты: используйте
.nest()
для создания логических групп - Следуйте RESTful конвенциям: используйте правильные HTTP-методы и структуру путей
- Используйте версионирование: добавляйте версию в путь или заголовки для обратной совместимости
- Соблюдайте консистентность: следуйте единому стилю именования во всем API
Оптимизация производительности
- Избегайте слишком глубокой вложенности: чрезмерная вложенность роутеров может снизить производительность
- Используйте параметры пути экономно: слишком много динамических сегментов может усложнить логику и снизить кэшируемость
Безопасность
- Валидируйте параметры пути: используйте типизированные параметры для ранней проверки и предотвращения неожиданных входных данных
- Защищайте чувствительные маршруты: применяйте middleware для аутентификации и авторизации
Заключение
Система маршрутизации Axum предоставляет мощные инструменты для организации HTTP-эндпоинтов вашего приложения. Комбинируя роутеры с методами .route()
, .nest()
и .merge()
, вы можете создавать сложные и хорошо организованные API.
В следующих разделах мы рассмотрим, как обрабатывать запросы с помощью обработчиков и как извлекать данные из запросов с помощью экстракторов.