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.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.