Структура проекта в 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
:
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
:
mod model;
mod routes;
mod handlers;
mod repository;
pub use model::{User, CreateUser, UpdateUser};
pub use routes::create_routes;
Пример src/users/routes.rs
:
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
Пример разделения ответственности в слоях:
// 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:
- Используйте файл
mod.rs
для экспорта публичных интерфейсов модуля:
// users/mod.rs
mod model;
mod handlers;
mod repository;
mod service;
pub use model::{User, NewUser};
pub use handlers::routes;
- Разделяйте публичные и приватные интерфейсы:
// В модуле
pub struct User {
pub id: Uuid,
pub username: String,
// Приватное поле, не доступное из других модулей
password_hash: String,
}
// При экспорте из модуля
pub use model::User;
Рекомендации для больших проектов
Для больших проектов рекомендуется:
- Рабочее пространство Cargo (workspace) — для разделения на несколько пакетов:
# Cargo.toml в корне проекта
[workspace]
members = [
"app", # Основное приложение
"core", # Бизнес-логика
"db", # Взаимодействие с БД
"api-models", # Структуры для API
"migration", # Миграции БД
]
- Feature Flags — для опциональных возможностей:
[features]
default = ["postgres"]
postgres = ["sqlx/postgres"]
sqlite = ["sqlx/sqlite"]
kafka = ["rdkafka"]
metrics = ["prometheus"]
- Инверсия зависимостей — для упрощения тестирования:
// Определение трейта
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
}
}
Управление конфигурацией
Организуйте конфигурацию приложения следующим образом:
// 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());
Обработка зависимостей
Для управления зависимостями используйте состояние приложения:
// 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. Организация роутов
Собирайте маршруты иерархически:
// 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
// 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 и модели данных:
// 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. Централизованная обработка ошибок
// 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. Комментирование и документация
/// Представляет пользователя в системе
///
/// # 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-приложений, особенно в долгосрочной перспективе. Выбор конкретной организации кода зависит от размера проекта, команды и бизнес-требований. Важно соблюдать принципы чистой архитектуры, разделяя ответственность между слоями приложения.