Skip to content

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

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

Содержание

Базовая структура

Минимальная структура проекта на Axum:

project_name/
├── Cargo.toml
├── .gitignore
├── src/
│   ├── main.rs       # Точка входа приложения
│   ├── routes.rs     # Определение маршрутов
│   ├── handlers.rs   # Обработчики запросов
│   ├── models.rs     # Модели данных
│   └── lib.rs        # Общие функции и типы
├── tests/            # Интеграционные тесты
├── .env              # Переменные окружения
└── README.md         # Документация

Пример main.rs:

rust
mod routes;
mod handlers;
mod models;

use axum::{Router, Server};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    // Инициализация логирования
    tracing_subscriber::fmt::init();
    
    // Создание роутера
    let app = routes::create_router();
    
    // Настройка адреса
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    tracing::info!("Сервер запущен на http://{}", addr);
    
    // Запуск сервера
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Структурирование по функциональности

Для более крупных проектов рекомендуется структурирование по функциональности (features):

project_name/
├── Cargo.toml
├── src/
│   ├── main.rs
│   ├── lib.rs
│   ├── config.rs         # Конфигурация приложения
│   ├── error.rs          # Типы ошибок и их обработка
│   ├── users/            # Модуль для работы с пользователями
│   │   ├── mod.rs
│   │   ├── model.rs
│   │   ├── routes.rs
│   │   ├── handlers.rs
│   │   └── repository.rs
│   ├── products/         # Модуль для работы с товарами
│   │   ├── mod.rs
│   │   ├── model.rs
│   │   ├── routes.rs
│   │   ├── handlers.rs
│   │   └── repository.rs
│   ├── auth/             # Модуль для аутентификации
│   │   ├── mod.rs
│   │   ├── middleware.rs
│   │   └── service.rs
│   └── common/           # Общие компоненты
│       ├── mod.rs
│       ├── validation.rs
│       └── pagination.rs
├── migrations/           # Миграции базы данных
├── tests/
└── .env

Пример src/users/mod.rs:

rust
mod model;
mod routes;
mod handlers;
mod repository;

pub use model::{User, CreateUser, UpdateUser};
pub use routes::create_routes;

Пример src/users/routes.rs:

rust
use axum::{
    Router,
    routing::{get, post, put, delete},
};
use crate::users::handlers;

pub fn create_routes() -> Router {
    Router::new()
        .route("/users", get(handlers::list_users))
        .route("/users", post(handlers::create_user))
        .route("/users/:id", get(handlers::get_user))
        .route("/users/:id", put(handlers::update_user))
        .route("/users/:id", delete(handlers::delete_user))
}

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

Альтернативный подход — структурирование по слоям архитектуры:

project_name/
├── Cargo.toml
├── src/
│   ├── main.rs
│   ├── lib.rs
│   ├── config/          # Настройки приложения
│   │   ├── mod.rs
│   │   └── app_config.rs
│   ├── api/             # API слой
│   │   ├── mod.rs 
│   │   ├── routes.rs    # Настройка маршрутов
│   │   ├── handlers.rs  # Обработчики запросов
│   │   └── middlewares/ # Middleware компоненты
│   ├── domain/          # Бизнес-логика
│   │   ├── mod.rs
│   │   ├── users.rs
│   │   ├── products.rs
│   │   └── orders.rs
│   ├── models/          # Структуры данных
│   │   ├── mod.rs
│   │   ├── user.rs
│   │   ├── product.rs
│   │   └── order.rs
│   ├── repositories/    # Слой доступа к данным
│   │   ├── mod.rs
│   │   ├── user_repo.rs
│   │   ├── product_repo.rs
│   │   └── order_repo.rs
│   └── utils/           # Вспомогательные функции
│       ├── mod.rs
│       └── validation.rs
├── migrations/
├── tests/
└── .env

Пример разделения ответственности в слоях:

rust
// api/handlers.rs - только обработка HTTP
async fn create_user(
    Json(payload): Json<CreateUserRequest>,
    State(state): State<AppState>,
) -> Result<Json<UserResponse>, ApiError> {
    let user = state.user_service.create_user(payload.into()).await?;
    Ok(Json(user.into()))
}

// domain/users.rs - бизнес-логика
pub async fn create_user(
    db: &DbPool,
    user_data: CreateUserDto,
) -> Result<User, DomainError> {
    // Валидация, бизнес-правила
    validate_user(&user_data)?;
    
    // Вызов репозитория для сохранения
    let user = user_repository::create(db, user_data).await?;
    
    // Дополнительная логика (например, отправка уведомлений)
    send_welcome_email(&user)?;
    
    Ok(user)
}

// repositories/user_repo.rs - работа с БД
pub async fn create(
    db: &DbPool, 
    user: CreateUserDto,
) -> Result<User, DbError> {
    // Код для сохранения в БД
}

Организация модулей

Следуйте общим принципам организации кода в Rust:

  1. Используйте файл mod.rs для экспорта публичных интерфейсов модуля:
rust
// users/mod.rs
mod model;
mod handlers;
mod repository;
mod service;

pub use model::{User, NewUser};
pub use handlers::routes;
  1. Разделяйте публичные и приватные интерфейсы:
rust
// В модуле
pub struct User {
    pub id: Uuid,
    pub username: String,
    // Приватное поле, не доступное из других модулей
    password_hash: String,
}

// При экспорте из модуля
pub use model::User;

Рекомендации для больших проектов

Для больших проектов рекомендуется:

  1. Рабочее пространство Cargo (workspace) — для разделения на несколько пакетов:
toml
# Cargo.toml в корне проекта
[workspace]
members = [
    "app",           # Основное приложение
    "core",          # Бизнес-логика
    "db",            # Взаимодействие с БД
    "api-models",    # Структуры для API
    "migration",     # Миграции БД
]
  1. Feature Flags — для опциональных возможностей:
toml
[features]
default = ["postgres"]
postgres = ["sqlx/postgres"]
sqlite = ["sqlx/sqlite"]
kafka = ["rdkafka"]
metrics = ["prometheus"]
  1. Инверсия зависимостей — для упрощения тестирования:
rust
// Определение трейта
pub trait UserRepository {
    async fn find_by_id(&self, id: Uuid) -> Result<User, Error>;
    async fn create(&self, user: NewUser) -> Result<User, Error>;
}

// Реализация для PostgreSQL
pub struct PgUserRepository {
    pool: PgPool,
}

impl UserRepository for PgUserRepository {
    // реализация методов
}

// Использование в сервисе
pub struct UserService<T: UserRepository> {
    repository: T,
}

impl<T: UserRepository> UserService<T> {
    pub fn new(repository: T) -> Self {
        Self { repository }
    }
    
    pub async fn get_user(&self, id: Uuid) -> Result<User, Error> {
        self.repository.find_by_id(id).await
    }
}

Управление конфигурацией

Организуйте конфигурацию приложения следующим образом:

rust
// config.rs
use serde::Deserialize;
use std::env;

#[derive(Clone, Debug, Deserialize)]
pub struct Config {
    pub server: ServerConfig,
    pub database: DatabaseConfig,
    pub auth: AuthConfig,
}

#[derive(Clone, Debug, Deserialize)]
pub struct ServerConfig {
    pub host: String,
    pub port: u16,
}

#[derive(Clone, Debug, Deserialize)]
pub struct DatabaseConfig {
    pub url: String,
    pub max_connections: u32,
}

#[derive(Clone, Debug, Deserialize)]
pub struct AuthConfig {
    pub jwt_secret: String,
    pub token_expiration: u64,
}

impl Config {
    pub fn from_env() -> Result<Self, config::ConfigError> {
        let mut cfg = config::Config::default();
        
        // Базовая конфигурация
        cfg.merge(config::File::with_name("config/default"))?;
        
        // Конфигурация для среды
        let env = env::var("APP_ENV").unwrap_or_else(|_| "development".into());
        cfg.merge(config::File::with_name(&format!("config/{}", env)).optional())?;
        
        // Переменные окружения
        cfg.merge(config::Environment::with_prefix("APP"))?;
        
        cfg.try_into()
    }
}

// Использование в main.rs
let config = Config::from_env().expect("Ошибка загрузки конфигурации");
let app_state = AppState::new(config.clone());

Обработка зависимостей

Для управления зависимостями используйте состояние приложения:

rust
// app_state.rs
use std::sync::Arc;

#[derive(Clone)]
pub struct AppState {
    config: Config,
    db_pool: Arc<PgPool>,
    user_service: Arc<UserService>,
    product_service: Arc<ProductService>,
}

impl AppState {
    pub async fn new(config: Config) -> Result<Self, Error> {
        // Создание пула соединений БД
        let db_pool = PgPoolOptions::new()
            .max_connections(config.database.max_connections)
            .connect(&config.database.url)
            .await?;
            
        // Создание репозиториев
        let user_repo = Arc::new(PgUserRepository::new(db_pool.clone()));
        let product_repo = Arc::new(PgProductRepository::new(db_pool.clone()));
        
        // Создание сервисов
        let user_service = Arc::new(UserService::new(user_repo));
        let product_service = Arc::new(ProductService::new(product_repo));
        
        Ok(Self {
            config,
            db_pool: Arc::new(db_pool),
            user_service,
            product_service,
        })
    }
}

// Использование в main.rs
let app_state = AppState::new(config).await?;

let app = Router::new()
    .nest("/api", api_routes())
    .with_state(app_state);

Примеры структур реальных проектов

1. Монолитное API-приложение

my_api/
├── Cargo.toml
├── .env
├── config/
│   ├── default.toml
│   ├── development.toml
│   └── production.toml
├── src/
│   ├── main.rs
│   ├── config.rs
│   ├── error.rs
│   ├── app_state.rs
│   ├── server.rs
│   ├── api/
│   │   ├── mod.rs
│   │   ├── routes.rs
│   │   ├── validators.rs
│   │   ├── users.rs
│   │   ├── products.rs
│   │   └── orders.rs
│   ├── services/
│   │   ├── mod.rs
│   │   ├── users.rs
│   │   ├── products.rs
│   │   └── orders.rs
│   ├── repositories/
│   │   ├── mod.rs
│   │   ├── traits.rs
│   │   ├── postgres/
│   │   │   ├── mod.rs
│   │   │   ├── users.rs
│   │   │   ├── products.rs
│   │   │   └── orders.rs
│   │   └── redis/
│   │       ├── mod.rs
│   │       └── cache.rs
│   └── models/
│       ├── mod.rs
│       ├── db/
│       │   ├── mod.rs
│       │   ├── users.rs
│       │   ├── products.rs
│       │   └── orders.rs
│       └── api/
│           ├── mod.rs
│           ├── requests.rs
│           └── responses.rs
├── migrations/
│   ├── 20230101000000_create_users.sql
│   ├── 20230101000001_create_products.sql
│   └── 20230101000002_create_orders.sql
└── tests/
    ├── api/
    │   ├── users.rs
    │   ├── products.rs
    │   └── orders.rs
    └── repositories/
        ├── users.rs
        ├── products.rs
        └── orders.rs

2. Микросервисная архитектура

my_services/
├── Cargo.toml  # Workspace
├── common/     # Общие компоненты
│   ├── Cargo.toml
│   └── src/
│       ├── lib.rs
│       ├── error.rs
│       └── models/
├── user-service/
│   ├── Cargo.toml
│   └── src/
│       ├── main.rs
│       ├── config.rs
│   │   ├── api/
│   │   ├── services/
│   │   └── repositories/
│   ├── product-service/
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── main.rs
│   │       ├── config.rs
│   │       ├── api/
│   │       ├── services/
│   │       └── repositories/
│   └── api-gateway/
│       ├── Cargo.toml
│       └── src/
│           ├── main.rs
│           ├── config.rs
│           └── routes/

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

1. Организация роутов

Собирайте маршруты иерархически:

rust
// main.rs
let app = Router::new()
    .nest("/api", api_routes())
    .layer(TraceLayer::new_for_http());

// api/routes.rs
pub fn api_routes() -> Router {
    Router::new()
        .nest("/users", users::routes())
        .nest("/products", products::routes())
        .nest("/orders", orders::routes())
        .route("/health", get(health_check))
}

// api/users.rs
pub fn routes() -> Router {
    Router::new()
        .route("/", get(list_users).post(create_user))
        .route("/:id", get(get_user).put(update_user).delete(delete_user))
}

2. Отделение бизнес-логики от API

rust
// api/handlers.rs
async fn create_user(
    Json(payload): Json<CreateUserRequest>,
    State(state): State<AppState>,
) -> Result<Json<UserResponse>, ApiError> {
    let user = state.user_service.create_user(payload.into()).await?;
    Ok(Json(user.into()))
}

// services/users.rs
impl UserService {
    pub async fn create_user(&self, user_data: NewUser) -> Result<User, ServiceError> {
        // Бизнес-логика
        self.validate_user(&user_data)?;
        let user = self.repository.create(user_data).await?;
        self.event_publisher.publish_user_created(&user).await?;
        Ok(user)
    }
}

3. Структуры запросов и ответов

Разделяйте модели API и модели данных:

rust
// models/api/requests.rs
#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
    pub username: String,
    pub email: String,
    pub password: String,
}

// models/api/responses.rs
#[derive(Debug, Serialize)]
pub struct UserResponse {
    pub id: Uuid,
    pub username: String,
    pub email: String,
    pub created_at: DateTime<Utc>,
}

// models/db/users.rs
#[derive(Debug, sqlx::FromRow)]
pub struct User {
    pub id: Uuid,
    pub username: String,
    pub email: String,
    pub password_hash: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

// Преобразования
impl From<CreateUserRequest> for NewUser {
    fn from(req: CreateUserRequest) -> Self {
        Self {
            username: req.username,
            email: req.email,
            password: req.password,
        }
    }
}

impl From<User> for UserResponse {
    fn from(user: User) -> Self {
        Self {
            id: user.id,
            username: user.username,
            email: user.email,
            created_at: user.created_at,
        }
    }
}

4. Централизованная обработка ошибок

rust
// error.rs
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
    #[error("Неавторизованный запрос")]
    Unauthorized,
    
    #[error("Ресурс не найден: {0}")]
    NotFound(String),
    
    #[error("Неверный запрос: {0}")]
    BadRequest(String),
    
    #[error("Внутренняя ошибка сервера")]
    InternalError(#[from] anyhow::Error),
    
    #[error("Ошибка базы данных: {0}")]
    DatabaseError(#[from] sqlx::Error),
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let status = match &self {
            ApiError::Unauthorized => StatusCode::UNAUTHORIZED,
            ApiError::NotFound(_) => StatusCode::NOT_FOUND,
            ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
            ApiError::InternalError(_) | ApiError::DatabaseError(_) => {
                // Логируем внутренние ошибки
                tracing::error!("Internal error: {:?}", self);
                StatusCode::INTERNAL_SERVER_ERROR
            }
        };

        let body = Json(json!({
            "error": self.to_string()
        }));

        (status, body).into_response()
    }
}

5. Комментирование и документация

rust
/// Представляет пользователя в системе
///
/// # Fields
///
/// * `id` - Уникальный идентификатор пользователя
/// * `username` - Имя пользователя, должно быть уникальным
/// * `email` - Email пользователя, должен быть уникальным
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: Uuid,
    pub username: String,
    pub email: String,
}

/// Создает нового пользователя
///
/// # Arguments
///
/// * `payload` - Данные для создания пользователя
/// * `state` - Состояние приложения с доступом к сервисам
///
/// # Returns
///
/// Возвращает объект пользователя в случае успешного создания
/// или ошибку, если создание не удалось
///
/// # Errors
///
/// Возвращает `ApiError::BadRequest` если данные некорректны
/// или `ApiError::InternalError` при ошибке сохранения
pub async fn create_user(
    Json(payload): Json<CreateUserRequest>,
    State(state): State<AppState>,
) -> Result<Json<UserResponse>, ApiError> {
    // Реализация
}

Правильная структура проекта значительно упрощает разработку и поддержку Axum-приложений, особенно в долгосрочной перспективе. Выбор конкретной организации кода зависит от размера проекта, команды и бизнес-требований. Важно соблюдать принципы чистой архитектуры, разделяя ответственность между слоями приложения.