项目结构 #

基础项目结构 #

一个典型的 Actix Web 项目结构如下:

text
my_actix_app/
├── Cargo.toml              # 项目配置
├── Cargo.lock              # 依赖锁定
├── .env                    # 环境变量
├── .gitignore              # Git 忽略文件
├── src/
│   ├── main.rs             # 入口文件
│   ├── lib.rs              # 库入口(可选)
│   ├── config.rs           # 配置模块
│   ├── error.rs            # 错误处理
│   ├── handlers/           # 处理函数
│   │   ├── mod.rs
│   │   ├── user.rs
│   │   └── auth.rs
│   ├── models/             # 数据模型
│   │   ├── mod.rs
│   │   └── user.rs
│   ├── services/           # 业务逻辑
│   │   ├── mod.rs
│   │   └── user.rs
│   ├── repositories/       # 数据访问
│   │   ├── mod.rs
│   │   └── user.rs
│   ├── middleware/         # 中间件
│   │   └── mod.rs
│   └── utils/              # 工具函数
│       └── mod.rs
├── tests/                  # 集成测试
│   └── integration_test.rs
├── migrations/             # 数据库迁移
└── static/                 # 静态文件

模块划分原则 #

分层架构 #

text
┌─────────────────────────────────────────────────────────────┐
│                        Handlers                              │
│                    (处理 HTTP 请求)                          │
├─────────────────────────────────────────────────────────────┤
│                        Services                              │
│                    (业务逻辑层)                              │
├─────────────────────────────────────────────────────────────┤
│                      Repositories                            │
│                    (数据访问层)                              │
├─────────────────────────────────────────────────────────────┤
│                        Models                                │
│                    (数据模型层)                              │
└─────────────────────────────────────────────────────────────┘

详细示例 #

Cargo.toml #

toml
[package]
name = "my_actix_app"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"
actix-rt = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
dotenvy = "0.15"
env_logger = "0.10"
log = "0.4"
thiserror = "1"
anyhow = "1"

[dev-dependencies]
actix-rt = "2"

src/main.rs #

rust
mod config;
mod error;
mod handlers;
mod middleware;
mod models;
mod repositories;
mod services;
mod utils;

use actix_web::{web, App, HttpServer};
use std::sync::Arc;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenvy::dotenv().ok();
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
    
    let config = config::Config::from_env();
    let db_pool = repositories::create_pool(&config.database_url).await;
    
    log::info!("Starting server at http://{}:{}", config.host, config.port);
    
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(Arc::new(db_pool.clone())))
            .app_data(web::Data::new(config.clone()))
            .configure(handlers::config)
            .wrap(middleware::logger::Logger::default())
    })
    .bind(format!("{}:{}", config.host, config.port))?
    .workers(4)
    .run()
    .await
}

src/config.rs #

rust
use serde::Deserialize;

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

impl Config {
    pub fn from_env() -> Self {
        Self {
            host: std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
            port: std::env::var("PORT")
                .unwrap_or_else(|_| "8080".to_string())
                .parse()
                .unwrap_or(8080),
            database_url: std::env::var("DATABASE_URL")
                .expect("DATABASE_URL must be set"),
        }
    }
}

src/error.rs #

rust
use actix_web::{HttpResponse, ResponseError};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Not found: {0}")]
    NotFound(String),
    
    #[error("Bad request: {0}")]
    BadRequest(String),
    
    #[error("Internal server error: {0}")]
    InternalError(String),
    
    #[error("Database error: {0}")]
    DatabaseError(#[from] sqlx::Error),
}

impl ResponseError for AppError {
    fn error_response(&self) -> HttpResponse {
        match self {
            AppError::NotFound(msg) => HttpResponse::NotFound().json(serde_json::json!({
                "error": msg
            })),
            AppError::BadRequest(msg) => HttpResponse::BadRequest().json(serde_json::json!({
                "error": msg
            })),
            AppError::InternalError(msg) => HttpResponse::InternalServerError().json(serde_json::json!({
                "error": msg
            })),
            AppError::DatabaseError(e) => HttpResponse::InternalServerError().json(serde_json::json!({
                "error": e.to_string()
            })),
        }
    }
}

pub type AppResult<T> = Result<T, AppError>;

src/models/mod.rs #

rust
pub mod user;

pub use user::*;

src/models/user.rs #

rust
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: i32,
    pub name: String,
    pub email: String,
    pub created_at: chrono::NaiveDateTime,
}

#[derive(Debug, Deserialize)]
pub struct CreateUser {
    pub name: String,
    pub email: String,
}

#[derive(Debug, Deserialize)]
pub struct UpdateUser {
    pub name: Option<String>,
    pub email: Option<String>,
}

src/handlers/mod.rs #

rust
mod user;
mod health;

use actix_web::web;

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api")
            .configure(user::config)
            .configure(health::config)
    );
}

src/handlers/health.rs #

rust
use actix_web::{web, HttpResponse, Responder};

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.route("/health", web::get().to(health));
}

async fn health() -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({
        "status": "healthy"
    }))
}

src/handlers/user.rs #

rust
use actix_web::{web, HttpResponse, Responder};
use crate::models::{CreateUser, UpdateUser, User};
use crate::services::UserService;
use crate::error::AppResult;
use std::sync::Arc;

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg
        .route("/users", web::get().to(list_users))
        .route("/users", web::post().to(create_user))
        .route("/users/{id}", web::get().to(get_user))
        .route("/users/{id}", web::put().to(update_user))
        .route("/users/{id}", web::delete().to(delete_user));
}

async fn list_users(
    service: web::Data<Arc<UserService>>,
) -> AppResult<impl Responder> {
    let users = service.list_users().await?;
    Ok(HttpResponse::Ok().json(users))
}

async fn get_user(
    path: web::Path<i32>,
    service: web::Data<Arc<UserService>>,
) -> AppResult<impl Responder> {
    let id = path.into_inner();
    let user = service.get_user(id).await?;
    Ok(HttpResponse::Ok().json(user))
}

async fn create_user(
    body: web::Json<CreateUser>,
    service: web::Data<Arc<UserService>>,
) -> AppResult<impl Responder> {
    let user = service.create_user(body.into_inner()).await?;
    Ok(HttpResponse::Created().json(user))
}

async fn update_user(
    path: web::Path<i32>,
    body: web::Json<UpdateUser>,
    service: web::Data<Arc<UserService>>,
) -> AppResult<impl Responder> {
    let id = path.into_inner();
    let user = service.update_user(id, body.into_inner()).await?;
    Ok(HttpResponse::Ok().json(user))
}

async fn delete_user(
    path: web::Path<i32>,
    service: web::Data<Arc<UserService>>,
) -> AppResult<impl Responder> {
    let id = path.into_inner();
    service.delete_user(id).await?;
    Ok(HttpResponse::NoContent().finish())
}

src/services/mod.rs #

rust
mod user;

pub use user::UserService;

src/services/user.rs #

rust
use crate::error::{AppError, AppResult};
use crate::models::{CreateUser, UpdateUser, User};
use crate::repositories::UserRepository;
use std::sync::Arc;

pub struct UserService {
    repo: Arc<UserRepository>,
}

impl UserService {
    pub fn new(repo: Arc<UserRepository>) -> Self {
        Self { repo }
    }

    pub async fn list_users(&self) -> AppResult<Vec<User>> {
        self.repo.find_all().await
    }

    pub async fn get_user(&self, id: i32) -> AppResult<User> {
        self.repo.find_by_id(id).await?
            .ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))
    }

    pub async fn create_user(&self, input: CreateUser) -> AppResult<User> {
        self.repo.create(input).await
    }

    pub async fn update_user(&self, id: i32, input: UpdateUser) -> AppResult<User> {
        self.repo.update(id, input).await
    }

    pub async fn delete_user(&self, id: i32) -> AppResult<()> {
        self.repo.delete(id).await
    }
}

src/repositories/mod.rs #

rust
mod user;

pub use user::UserRepository;
use sqlx::postgres::PgPoolOptions;

pub async fn create_pool(database_url: &str) -> sqlx::PgPool {
    PgPoolOptions::new()
        .max_connections(5)
        .connect(database_url)
        .await
        .expect("Failed to create pool")
}

src/repositories/user.rs #

rust
use crate::error::{AppError, AppResult};
use crate::models::{CreateUser, UpdateUser, User};
use sqlx::PgPool;

pub struct UserRepository {
    pool: PgPool,
}

impl UserRepository {
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }

    pub async fn find_all(&self) -> AppResult<Vec<User>> {
        let users = sqlx::query_as!(
            User,
            "SELECT id, name, email, created_at FROM users ORDER BY id"
        )
        .fetch_all(&self.pool)
        .await?;
        Ok(users)
    }

    pub async fn find_by_id(&self, id: i32) -> AppResult<Option<User>> {
        let user = sqlx::query_as!(
            User,
            "SELECT id, name, email, created_at FROM users WHERE id = $1",
            id
        )
        .fetch_optional(&self.pool)
        .await?;
        Ok(user)
    }

    pub async fn create(&self, input: CreateUser) -> AppResult<User> {
        let user = sqlx::query_as!(
            User,
            r#"
            INSERT INTO users (name, email, created_at)
            VALUES ($1, $2, NOW())
            RETURNING id, name, email, created_at
            "#,
            input.name,
            input.email
        )
        .fetch_one(&self.pool)
        .await?;
        Ok(user)
    }

    pub async fn update(&self, id: i32, input: UpdateUser) -> AppResult<User> {
        let existing = self.find_by_id(id).await?
            .ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))?;
        
        let user = sqlx::query_as!(
            User,
            r#"
            UPDATE users
            SET name = COALESCE($1, name),
                email = COALESCE($2, email)
            WHERE id = $3
            RETURNING id, name, email, created_at
            "#,
            input.name,
            input.email,
            id
        )
        .fetch_one(&self.pool)
        .await?;
        Ok(user)
    }

    pub async fn delete(&self, id: i32) -> AppResult<()> {
        let result = sqlx::query!("DELETE FROM users WHERE id = $1", id)
            .execute(&self.pool)
            .await?;
        
        if result.rows_affected() == 0 {
            return Err(AppError::NotFound(format!("User {} not found", id)));
        }
        Ok(())
    }
}

src/middleware/mod.rs #

rust
pub mod logger;

src/middleware/logger.rs #

rust
pub use actix_web::middleware::Logger;

.env #

text
HOST=127.0.0.1
PORT=8080
DATABASE_URL=postgres://user:password@localhost/myapp
RUST_LOG=info

.gitignore #

text
/target
/.env
*.log
.DS_Store

项目配置最佳实践 #

环境区分 #

text
.env.development    # 开发环境
.env.production     # 生产环境
.env.test           # 测试环境

日志配置 #

rust
// 开发环境:详细日志
RUST_LOG=debug,my_actix_app=trace

// 生产环境:简洁日志
RUST_LOG=info,my_actix_app=warn

下一步 #

现在你已经了解了项目结构,接下来学习 路由基础,深入了解 Actix Web 的路由系统!

最后更新:2026-03-29