AxumMiddleTechnical

Каковы распространённые паттерны организации большого приложения Axum по модулям?

Большие Axum-приложения организуют по принципу feature-модулей: каждый модуль содержит роутер, обработчики, модели и ошибки. Роутеры объединяются через Router::merge() или Router::nest() в корневом main.rs.

Организация большого приложения Axum по модулям

Нет единственно верной структуры, но есть устоявшиеся паттерны. Самый распространённый — организация по доменным модулям (feature-based), где каждая функциональная область (users, products, orders) живёт в своём модуле.

Структура проекта

src/
├── main.rs           // точка входа, сборка приложения
├── state.rs          // AppState — общее состояние
├── error.rs          // общий тип ошибки AppError
├── routes.rs         // сборка всех роутеров
├── middleware/
│   ├── mod.rs
│   ├── auth.rs       // JWT/session middleware
│   └── logging.rs
├── users/
│   ├── mod.rs
│   ├── router.rs     // users_router() -> Router
│   ├── handlers.rs   // async fn get_user, create_user...
│   ├── models.rs     // User, CreateUserRequest
│   └── service.rs    // бизнес-логика
├── products/
│   ├── mod.rs
│   ├── router.rs
│   ├── handlers.rs
│   └── models.rs
└── db/
    ├── mod.rs
    └── migrations.rs

Пример модуля users

// users/router.rs
use axum::{Router, routing::{get, post}};
use crate::state::AppState;
use super::handlers;

pub fn users_router() -> Router<AppState> {
    Router::new()
        .route("/users", get(handlers::list_users).post(handlers::create_user))
        .route("/users/:id", get(handlers::get_user).delete(handlers::delete_user))
}
// users/handlers.rs
use axum::{extract::{Path, State}, Json};
use crate::{state::AppState, error::AppError};
use super::models::{User, CreateUserRequest};

pub async fn get_user(
    State(state): State<AppState>,
    Path(id): Path<i64>,
) -> Result<Json<User>, AppError> {
    let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
        .bind(id)
        .fetch_one(&state.db)
        .await?;
    Ok(Json(user))
}

Сборка роутеров в routes.rs

// routes.rs
use axum::Router;
use crate::state::AppState;
use crate::{users, products};
use tower_http::trace::TraceLayer;

pub fn create_router(state: AppState) -> Router {
    Router::new()
        .nest("/api/v1", api_router())
        .with_state(state)
        .layer(TraceLayer::new_for_http())
}

fn api_router() -> Router<AppState> {
    Router::new()
        .merge(users::router::users_router())
        .merge(products::router::products_router())
}

Общий тип ошибки

// error.rs
use axum::{response::{IntoResponse, Response}, http::StatusCode, Json};
use serde_json::json;

pub enum AppError {
    NotFound(String),
    Database(sqlx::Error),
    Unauthorized,
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
            AppError::Database(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
            AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".into()),
        };
        (status, Json(json!({ "error": message }))).into_response()
    }
}

impl From<sqlx::Error> for AppError {
    fn from(e: sqlx::Error) -> Self { AppError::Database(e) }
}

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

  • Router::merge() не работает с маршрутами с конфликтующими путями — Axum паникует при старте, а не возвращает ошибку компиляции.
  • Типовой параметр роутера Router<AppState> должен совпадать — если забыть указать его в одном из sub-роутеров, компилятор выдаст трудночитаемую ошибку.
  • Вызов .with_state() должен быть последним перед axum::serve() — добавление слоёв после может изменить тип роутера.
  • Круговые зависимости между модулями (users зависит от orders, orders от users) требуют выноса общих типов в отдельный domain крейт.
  • Слишком мелкие модули создают boilerplate без выгоды — разумный минимум для отдельного модуля: 3+ маршрута или нетривиальная логика.
  • Тесты для обработчиков удобнее писать через axum::test::TestClient (или tower::ServiceExt::oneshot) — не забывайте про тестовые фикстуры состояния.

Common mistakes

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

What the interviewer is testing

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

Sources

Related topics