AxumMiddleTechnical

Какие наиболее часто используемые встроенные extractors в Axum (Path, Query, Json, State и др.)?

Основные экстракторы: Path (сегменты URL), Query (query string), Json (тело запроса), State (shared state через with_state). Из axum-extra: TypedHeader для типизированных заголовков. Json должен быть последним аргументом, так как потребляет body.

Встроенные экстракторы Axum

Axum предоставляет набор готовых экстракторов в модуле axum::extract. Каждый отвечает за конкретный источник данных в HTTP-запросе.

Path — параметры пути

Извлекает именованные сегменты URL. Тип должен реализовывать serde::Deserialize.

use axum::extract::Path;

// Одиночный параметр
async fn get_user(Path(id): Path<u32>) -> String {
    format!("User {id}")
}

// Несколько параметров через кортеж или структуру
async fn get_comment(
    Path((post_id, comment_id)): Path<(u32, u32)>,
) -> String {
    format!("Post {post_id}, comment {comment_id}")
}

// Маршруты
// .route("/users/:id", get(get_user))
// .route("/posts/:post_id/comments/:comment_id", get(get_comment))

Query — параметры строки запроса

Парсит query string в структуру. При отсутствии поля с #[serde(default)] возвращает 422.

use axum::extract::Query;
use serde::Deserialize;

#[derive(Deserialize)]
struct SearchParams {
    q: String,
    #[serde(default = "default_page")]
    page: u32,
    #[serde(default = "default_size")]
    size: u32,
}

fn default_page() -> u32 { 1 }
fn default_size() -> u32 { 20 }

async fn search(Query(params): Query<SearchParams>) -> String {
    format!("Search '{}', page {}, size {}", params.q, params.page, params.size)
}

Json — тело запроса

Десериализует JSON-тело. Требует заголовок Content-Type: application/json. Должен быть последним аргументом обработчика, так как потребляет body.

use axum::{extract::Json, response::IntoResponse, http::StatusCode};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct CreateItem { name: String, price: f64 }

#[derive(Serialize)]
struct Item { id: u32, name: String, price: f64 }

async fn create_item(Json(payload): Json<CreateItem>) -> impl IntoResponse {
    let item = Item { id: 1, name: payload.name, price: payload.price };
    (StatusCode::CREATED, Json(item))
}

State — разделяемое состояние приложения

Передаёт shared state (DB pool, конфиг, клиент) во все обработчики. Тип должен реализовывать Clone.

use axum::{extract::State, Router, routing::get};
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    db_pool: sqlx::PgPool,
    config: Arc<Config>,
}

async fn list_users(State(state): State<AppState>) -> impl IntoResponse {
    let users = sqlx::query_as::<_, User>("SELECT * FROM users")
        .fetch_all(&state.db_pool)
        .await
        .unwrap();
    Json(users)
}

let state = AppState { db_pool, config: Arc::new(config) };
let app = Router::new()
    .route("/users", get(list_users))
    .with_state(state);

Headers и TypedHeader

Для извлечения конкретных заголовков используется TypedHeader из крейта axum-extra:

use axum_extra::TypedHeader;
use headers::Authorization;
use headers::authorization::Bearer;

async fn auth_handler(
    TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> String {
    format!("Token: {}", auth.token())
}

Другие встроенные экстракторы

  • axum::extract::Form<T> — application/x-www-form-urlencoded тела
  • axum::extract::Multipart — multipart/form-data (загрузка файлов)
  • axum::extract::ConnectInfo<SocketAddr> — IP-адрес клиента
  • axum::extract::MatchedPath — шаблон пути, совпавший с маршрутом
  • axum::body::Bytes — сырое тело запроса
  • String — тело как UTF-8 строка
  • axum::extract::Request — весь запрос целиком

Подводные камни

  • Json<T> требует точный заголовок Content-Type: application/json; без него — 415 Unsupported Media Type, что неочевидно при отладке curl-запросов.
  • State<T> требует .with_state(value) при построении Router; если забыть, компилятор выдаст запутанную ошибку об отсутствии реализации трейта.
  • При использовании вложенных роутеров через .nest() State нужно добавлять на уровне каждого суб-роутера или передавать через Router::with_state() на верхнем уровне.
  • Path extractor возвращает 400, если тип не соответствует (например, строка вместо u32), но сообщение об ошибке по умолчанию минимально — стоит настраивать кастомный error handler.
  • Query с обязательным полем без #[serde(default)] вернёт 422 при отсутствующем параметре, что клиент может интерпретировать неверно.
  • Multipart нужно обрабатывать итерационно через .next_field().await; попытка получить все поля сразу не поддерживается.
  • ConnectInfo<SocketAddr> работает только если сервер запущен через axum::serve(...).into_make_service_with_connect_info(), иначе паникует.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics