Skip to content

Query параметры

Query параметры — важная часть URL, которая позволяет передавать данные в HTTP-запросах методом GET. Axum предоставляет удобные инструменты для работы с query-параметрами, автоматически извлекая и преобразуя их в Rust-структуры.

Основы работы с Query параметрами

В Axum для извлечения query-параметров используется экстрактор Query<T>, где T - тип, в который будут десериализованы параметры:

rust
use axum::{
    extract::Query,
    routing::get,
    Router,
};
use serde::Deserialize;

// Определяем структуру для query-параметров
#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    per_page: Option<u32>,
}

// Обработчик с извлечением query-параметров
async fn list_items(Query(params): Query<Pagination>) -> String {
    let page = params.page.unwrap_or(1);
    let per_page = params.per_page.unwrap_or(10);
    
    format!("Страница: {}, Элементов на странице: {}", page, per_page)
}

// Регистрация маршрута
let app = Router::new()
    .route("/items", get(list_items));

Этот код обрабатывает запросы вида /items?page=2&per_page=20.

Обработка простых типов

Для обработки одиночных параметров можно использовать простые типы:

rust
// Извлечение одиночного параметра
#[derive(Deserialize)]
struct IdParam {
    id: String,
}

async fn get_by_id(Query(params): Query<IdParam>) -> String {
    format!("ID: {}", params.id)
}

// Или с использованием HashMap
use std::collections::HashMap;

async fn generic_handler(Query(params): Query<HashMap<String, String>>) -> String {
    let mut response = String::new();
    
    for (key, value) in params {
        response.push_str(&format!("{}={}, ", key, value));
    }
    
    response
}

Вложенные структуры и массивы

Axum поддерживает сложные структуры в query-параметрах:

rust
#[derive(Deserialize)]
struct FilterParams {
    // Фильтр по диапазону дат
    from_date: Option<String>,
    to_date: Option<String>,
    
    // Фильтр по множеству категорий
    // Позволяет обрабатывать ?category=1&category=2&category=3
    category: Option<Vec<u32>>,
    
    // Фильтр по статусу
    status: Option<String>,
}

async fn filter_items(
    Query(params): Query<FilterParams>,
) -> String {
    let mut response = String::new();
    
    if let Some(from) = params.from_date {
        response.push_str(&format!("От: {}, ", from));
    }
    
    if let Some(to) = params.to_date {
        response.push_str(&format!("До: {}, ", to));
    }
    
    if let Some(categories) = params.category {
        response.push_str(&format!("Категории: {:?}, ", categories));
    }
    
    if let Some(status) = params.status {
        response.push_str(&format!("Статус: {}", status));
    }
    
    response
}

Преобразование и валидация

Serde позволяет выполнять преобразование и базовую валидацию параметров:

rust
use chrono::{DateTime, Utc};
use serde::Deserialize;

#[derive(Deserialize)]
struct SearchParams {
    // Поисковый запрос
    q: String,
    
    // Преобразование строки в дату
    #[serde(default)]
    #[serde(with = "chrono::serde::ts_seconds_option")]
    after: Option<DateTime<Utc>>,
    
    // Ограничение значений с помощью валидации
    #[serde(default = "default_limit")]
    #[serde(deserialize_with = "validate_limit")]
    limit: u32,
}

fn default_limit() -> u32 {
    10
}

fn validate_limit<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let limit: u32 = u32::deserialize(deserializer)?;
    
    // Ограничиваем максимальное значение
    if limit > 100 {
        return Ok(100);
    }
    
    // Минимальное значение
    if limit == 0 {
        return Ok(1);
    }
    
    Ok(limit)
}

async fn search(
    Query(params): Query<SearchParams>,
) -> String {
    format!(
        "Поиск: {}, После: {:?}, Лимит: {}",
        params.q,
        params.after,
        params.limit
    )
}

Обработка ошибок при разборе query-параметров

По умолчанию, если Axum не может десериализовать query-параметры, он возвращает ошибку 400 Bad Request. Можно настроить собственную обработку ошибок:

rust
use axum::{
    extract::{Query, rejection::QueryRejection},
    response::{IntoResponse, Response},
    http::StatusCode,
};
use serde_json::json;

async fn handler_with_error_handling(
    result: Result<Query<Pagination>, QueryRejection>,
) -> Response {
    match result {
        Ok(Query(params)) => {
            let page = params.page.unwrap_or(1);
            let per_page = params.per_page.unwrap_or(10);
            
            format!("Страница: {}, Элементов на странице: {}", page, per_page)
                .into_response()
        },
        Err(err) => {
            (
                StatusCode::BAD_REQUEST,
                json!({
                    "error": format!("Неверные параметры запроса: {}", err)
                }),
            ).into_response()
        }
    }
}

Опциональные параметры

Для необязательных параметров используйте Option:

rust
#[derive(Deserialize)]
struct QueryParams {
    // Обязательный параметр
    id: String,
    
    // Необязательные параметры
    name: Option<String>,
    age: Option<u32>,
    
    // Параметр со значением по умолчанию
    #[serde(default = "default_sort")]
    sort: String,
}

fn default_sort() -> String {
    "name".to_string()
}

async fn get_item(
    Query(params): Query<QueryParams>,
) -> String {
    let name_display = match params.name {
        Some(name) => format!("с именем '{}'", name),
        None => "без указания имени".to_string(),
    };
    
    let age_display = match params.age {
        Some(age) => format!("возраст: {}", age),
        None => "возраст не указан".to_string(),
    };
    
    format!(
        "Запрос элемента с ID: {} {} ({}), сортировка: {}",
        params.id,
        name_display,
        age_display,
        params.sort
    )
}

Использование Query с Path и другими экстракторами

Query часто используется в сочетании с другими экстракторами:

rust
use axum::{
    extract::{Path, Query, State},
    routing::get,
    Router,
};
use serde::Deserialize;

// Параметры пути
#[derive(Deserialize)]
struct UserParams {
    user_id: u64,
}

// Query-параметры
#[derive(Deserialize)]
struct UserQuery {
    include_details: Option<bool>,
}

// Состояние приложения
#[derive(Clone)]
struct AppState {
    // Например, соединение с БД
}

async fn get_user(
    Path(path): Path<UserParams>,
    Query(query): Query<UserQuery>,
    State(state): State<AppState>,
) -> String {
    let show_details = query.include_details.unwrap_or(false);
    
    format!(
        "Получение пользователя с ID: {}, показывать детали: {}",
        path.user_id,
        show_details
    )
}

// Регистрация маршрута
let app = Router::new()
    .route("/users/:user_id", get(get_user))
    .with_state(AppState { /* ... */ });

Сложные случаи использования

Фильтрация и сортировка

Query-параметры часто используются для фильтрации и сортировки данных:

rust
use serde::Deserialize;
use std::collections::HashMap;

#[derive(Deserialize)]
struct ListOptions {
    // Пагинация
    page: Option<u32>,
    per_page: Option<u32>,
    
    // Сортировка
    sort_by: Option<String>,
    order: Option<String>, // asc или desc
    
    // Фильтры - произвольные поля
    #[serde(flatten)]
    filters: HashMap<String, String>,
}

async fn list_resources(
    Query(options): Query<ListOptions>,
) -> String {
    let page = options.page.unwrap_or(1);
    let per_page = options.per_page.unwrap_or(10);
    let sort_by = options.sort_by.unwrap_or_else(|| "id".to_string());
    let order = options.order.unwrap_or_else(|| "asc".to_string());
    
    let mut filters_str = String::new();
    for (key, value) in options.filters {
        // Пропускаем служебные параметры
        if !["page", "per_page", "sort_by", "order"].contains(&key.as_str()) {
            filters_str.push_str(&format!("{}={}, ", key, value));
        }
    }
    
    format!(
        "Страница: {}, Элементов: {}, Сортировка: {} {}, Фильтры: {}",
        page, per_page, sort_by, order, filters_str
    )
}

Поиск с множеством параметров

Для сложного поиска с множеством опциональных параметров:

rust
#[derive(Deserialize)]
struct SearchOptions {
    // Поисковый запрос
    q: Option<String>,
    
    // Временной диапазон
    from: Option<String>,
    to: Option<String>,
    
    // Фильтры по категориям (множественный выбор)
    category: Option<Vec<String>>,
    
    // Фильтр по статусу
    status: Option<String>,
    
    // Геопозиция (для поиска ближайших)
    lat: Option<f64>,
    lng: Option<f64>,
    radius: Option<u32>,
}

async fn search(
    Query(options): Query<SearchOptions>,
) -> String {
    let mut search_description = String::new();
    
    if let Some(q) = options.q {
        search_description.push_str(&format!("Запрос: '{}', ", q));
    }
    
    // Дополнительная логика для обработки других параметров
    // ...
    
    if let Some(status) = options.status {
        search_description.push_str(&format!("Статус: {}, ", status));
    }
    
    // Проверка наличия геопозиции
    if options.lat.is_some() && options.lng.is_some() {
        let radius = options.radius.unwrap_or(10);
        search_description.push_str(
            &format!(
                "Поиск в радиусе {} км от координат ({}, {})",
                radius,
                options.lat.unwrap(),
                options.lng.unwrap()
            )
        );
    }
    
    search_description
}

Лучшие практики

Структурирование параметров

Для большей удобочитаемости и поддерживаемости группируйте связанные параметры в структуры:

rust
#[derive(Deserialize)]
struct ListParams {
    // Параметры пагинации
    #[serde(flatten)]
    pagination: Pagination,
    
    // Параметры сортировки
    #[serde(flatten)]
    sorting: Sorting,
    
    // Другие параметры
    // ...
}

#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    per_page: Option<u32>,
}

#[derive(Deserialize)]
struct Sorting {
    sort_by: Option<String>,
    order: Option<String>,
}

async fn list_handler(
    Query(params): Query<ListParams>,
) -> String {
    let page = params.pagination.page.unwrap_or(1);
    let per_page = params.pagination.per_page.unwrap_or(10);
    let sort_by = params.sorting.sort_by.unwrap_or_else(|| "id".to_string());
    let order = params.sorting.order.unwrap_or_else(|| "asc".to_string());
    
    // ...
    
    format!(
        "Страница: {}, Элементов: {}, Сортировка: {} {}",
        page, per_page, sort_by, order
    )
}

Валидация значений

Используйте библиотеку validator для валидации параметров:

rust
use validator::{Validate, ValidationError};

#[derive(Deserialize, Validate)]
struct SearchParams {
    #[validate(length(min = 2, message = "Поисковый запрос должен содержать не менее 2 символов"))]
    q: Option<String>,
    
    #[validate(range(min = 1, max = 100, message = "Лимит должен быть от 1 до 100"))]
    limit: Option<u32>,
    
    #[validate(custom = "validate_sort_field")]
    sort_by: Option<String>,
}

fn validate_sort_field(sort: &str) -> Result<(), ValidationError> {
    // Список разрешенных полей для сортировки
    let allowed_fields = ["id", "name", "created_at", "price"];
    
    if !allowed_fields.contains(&sort) {
        let mut err = ValidationError::new("invalid_sort_field");
        err.message = Some(format!(
            "Недопустимое поле для сортировки. Разрешены: {}",
            allowed_fields.join(", ")
        ).into());
        return Err(err);
    }
    
    Ok(())
}

async fn search(
    Query(params): Query<SearchParams>,
) -> Result<String, (StatusCode, String)> {
    // Валидация параметров
    if let Err(errors) = params.validate() {
        return Err((
            StatusCode::BAD_REQUEST,
            format!("Ошибка валидации: {:?}", errors),
        ));
    }
    
    // Обработка запроса
    let q = params.q.unwrap_or_default();
    let limit = params.limit.unwrap_or(10);
    let sort_by = params.sort_by.unwrap_or_else(|| "id".to_string());
    
    Ok(format!(
        "Поиск: '{}', Лимит: {}, Сортировка по: {}",
        q, limit, sort_by
    ))
}

Заключение

Query-параметры в Axum обеспечивают удобный и типобезопасный способ передачи данных в GET-запросах. Используя экстрактор Query<T> и возможности Serde, вы можете легко создавать API с гибкими параметрами фильтрации, сортировки и пагинации.

Ключевые преимущества подхода Axum:

  1. Автоматическая десериализация параметров в Rust-структуры
  2. Поддержка вложенных структур и массивов
  3. Возможность валидации и преобразования значений
  4. Типобезопасность и хорошая обработка ошибок

Правильная организация и валидация query-параметров делает ваш API удобным, предсказуемым и защищенным от ошибок.