项目结构 #
基础项目结构 #
一个典型的 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