Rust实战:使用Axum和SQLx构建高性能RESTful API
前言
Rust是一种现代系统编程语言,专注于性能、可靠性和生产力。它通过一套丰富的类型系统和所有权模型,在编译时保证内存安全和线程安全,让开发者能够自信地构建健壮的软件。Rust的设计哲学使其在需要高性能和高并发的场景中表现出色,例如Web后端服务、嵌入式系统和游戏开发。
在现代Web开发中,构建高性能、高并发的API服务是至关重要的。Rust凭借其内存安全、卓越性能和强大的并发能力,成为构建这类服务的理想选择。本文将通过一个实战项目,带您深入了解如何使用Axum框架和SQLx库构建一个功能完备、性能卓越的RESTful API。
Axum是一个基于Tokio的模块化Web框架,以其高性能和易用性著称。SQLx则是一个异步的、类型安全的SQL客户端,能够有效防止SQL注入等安全问题。通过结合这两者,我们将构建一个待办事项(Todo)管理API,涵盖CRUD(创建、读取、更新、删除)操作,并实现错误处理、数据验证和数据库交互等核心功能。

文章目录
- 前言
- 第一部分:项目初始化与环境配置
- 1.1 创建新项目
- 1.2 添加依赖
- 1.3 配置环境变量
- 1.4 数据库迁移
- 第二部分:构建核心业务逻辑
- 2.1 定义数据模型
- 2.2 实现数据库操作
- 2.3 错误处理
- 第三部分:创建API路由和处理器
- 3.1 创建路由
- 3.2 实现处理器函数
- 创建Todo
- 获取所有Todo
- 获取、更新和删除单个Todo
- 第四部分:运行与测试
- 4.1 运行API服务
- 4.2 使用cURL进行测试
- 创建一个新的Todo
- 获取所有Todo
- 总结
第一部分:项目初始化与环境配置
1.1 创建新项目
首先,我们使用Cargo创建一个新的Rust项目:
cargo new todo_api
cd todo_api

1.2 添加依赖
接下来,在Cargo.toml文件中添加项目所需的依赖:
[package]
name = "todo_api"
version = "0.1.0"
edition = "2021"[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] }
dotenv = "0.15"
thiserror = "1"
anyhow = "1"
validator = { version = "0.16", features = ["derive"] }

依赖项说明:
axum:核心Web框架。tokio:异步运行时。serde、serde_json:用于JSON序列化和反序列化。sqlx:异步数据库客户端,我们使用SQLite作为示例数据库。dotenv:用于管理环境变量。thiserror、anyhow:用于优雅地处理错误。validator:用于数据验证。
1.3 配置环境变量
在项目根目录下创建一个.env文件,用于存放数据库连接信息:
DATABASE_URL=sqlite:todos.db
同时,创建一个.sqlx目录,并在其中创建一个migrations文件夹,用于存放数据库迁移文件:
md -p .sqlx/migrations

1.4 数据库迁移
我们使用sqlx-cli工具来管理数据库迁移。首先,安装sqlx-cli:
cargo install sqlx-cli
然后,创建一个新的迁移文件:
sqlx migrate add create_todos_table

这会在.sqlx/migrations目录下生成一个SQL文件。编辑该文件,定义todos表的结构:
-- .sqlx/migrations/{timestamp}_create_todos_table.sql
CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT,title VARCHAR(255) NOT NULL,completed BOOLEAN NOT NULL DEFAULT FALSE
);
现在,运行迁移以创建数据库和表:
sqlx migrate run

第二部分:构建核心业务逻辑
2.1 定义数据模型
在src/main.rs中,我们首先定义Todo模型和用于创建、更新Todo项的数据传输对象(DTO)。
use serde::{Deserialize, Serialize};
use validator::Validate;#[derive(Serialize, sqlx::FromRow)]
struct Todo {id: i64,title: String,completed: bool,
}#[derive(Deserialize, Validate)]
struct CreateTodo {#[validate(length(min = 1, message = "Title is required"))]title: String,
}#[derive(Deserialize, Validate)]
struct UpdateTodo {#[validate(length(min = 1, message = "Title is required"))]title: Option<String>,completed: Option<bool>,
}
2.2 实现数据库操作
接下来,我们创建数据库连接池,并实现与数据库交互的函数。
use sqlx::{sqlite::SqlitePool, Error as SqlxError};
use std::env;async fn create_todo_db(pool: &SqlitePool, title: &str) -> Result<Todo, SqlxError> {let mut conn = pool.acquire().await?;let id = sqlx::query!("INSERT INTO todos (title) VALUES (?)",title).execute(&mut conn).await?.last_insert_rowid();let todo = sqlx::query_as!(Todo, "SELECT * FROM todos WHERE id = ?", id).fetch_one(&mut conn).await?;Ok(todo)
}async fn get_all_todos_db(pool: &SqlitePool) -> Result<Vec<Todo>, SqlxError> {let todos = sqlx::query_as!(Todo, "SELECT * FROM todos").fetch_all(pool).await?;Ok(todos)
}// 其他数据库操作函数(get_todo_by_id, update_todo_db, delete_todo_db)将在后续实现
2.3 错误处理
为了提供清晰的错误信息,我们定义一个自定义的错误类型。
use axum::{http::StatusCode,response::{IntoResponse, Response},Json,
};
use thiserror::Error;#[derive(Error, Debug)]
enum AppError {#[error("SQLx error: {0}")]Sqlx(#[from] SqlxError),#[error("Validation error: {0}")]Validation(#[from] validator::ValidationErrors),#[error("Item not found")]NotFound,
}impl IntoResponse for AppError {fn into_response(self) -> Response {let (status, error_message) = match self {AppError::Sqlx(_) => (StatusCode::INTERNAL_SERVER_ERROR,"Database error".to_string(),),AppError::Validation(err) => (StatusCode::BAD_REQUEST,format!("Validation failed: {}", err),),AppError::NotFound => (StatusCode::NOT_FOUND, "Item not found".to_string()),};let body = Json(serde_json::json!({ "error": error_message }));(status, body).into_response()}
}
第三部分:创建API路由和处理器
3.1 创建路由
在main函数中,我们设置Axum路由,并将它们与相应的处理器函数关联起来。
use axum::{routing::{get, post},Router,
};#[tokio::main]
async fn main() {dotenv::dotenv().ok();let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");let pool = SqlitePool::connect(&database_url).await.expect("Failed to create pool.");let app = Router::new().route("/todos", get(get_all_todos).post(create_todo)).route("/todos/:id", get(get_todo_by_id).patch(update_todo).delete(delete_todo)).with_state(pool);let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();println!("Listening on {}", listener.local_addr().unwrap());axum::serve(listener, app).await.unwrap();
}
3.2 实现处理器函数
现在,我们为每个路由实现处理器函数。
创建Todo
use axum::{extract::State, Json};async fn create_todo(State(pool): State<SqlitePool>,Json(payload): Json<CreateTodo>,
) -> Result<Json<Todo>, AppError> {payload.validate()?;let todo = create_todo_db(&pool, &payload.title).await?;Ok(Json(todo))
}
获取所有Todo
async fn get_all_todos(State(pool): State<SqlitePool>,
) -> Result<Json<Vec<Todo>>, AppError> {let todos = get_all_todos_db(&pool).await?;Ok(Json(todos))
}
获取、更新和删除单个Todo
这些函数的实现留给读者作为练习。您需要:
- 从路径中提取
id。 - 实现
get_todo_by_id_db、update_todo_db和delete_todo_db函数。 - 在处理器函数中调用这些数据库函数,并处理可能出现的
NotFound错误。
程序完整代码如下
use axum::{extract::{Path, State},http::StatusCode,response::{IntoResponse, Response},routing::get,Json, Router,};use serde::{Deserialize, Serialize};use sqlx::{sqlite::SqlitePool, Error as SqlxError};use std::env;use thiserror::Error;use validator::Validate;#[derive(Serialize, sqlx::FromRow)]struct Todo {id: i64,title: String,completed: bool,}#[derive(Deserialize, Validate)]struct CreateTodo {#[validate(length(min = 1, message = "Title is required"))]title: String,}#[derive(Deserialize, Validate)]struct UpdateTodo {#[validate(length(min = 1, message = "Title is required"))]title: Option<String>,completed: Option<bool>,}#[derive(Error, Debug)]enum AppError {#[error("SQLx error: {0}")]Sqlx(#[from] SqlxError),#[error("Validation error: {0}")]Validation(#[from] validator::ValidationErrors),#[error("Item not found")]NotFound,}impl IntoResponse for AppError {fn into_response(self) -> Response {let (status, error_message) = match self {AppError::Sqlx(_) => (StatusCode::INTERNAL_SERVER_ERROR,"Database error".to_string(),),AppError::Validation(err) => (StatusCode::BAD_REQUEST,format!("Validation failed: {}", err),),AppError::NotFound => (StatusCode::NOT_FOUND, "Item not found".to_string()),};let body = Json(serde_json::json!({ "error": error_message }));(status, body).into_response()}}async fn create_todo_db(pool: &SqlitePool, title: &str) -> Result<Todo, SqlxError> {let mut conn = pool.acquire().await?;let id = sqlx::query!("INSERT INTO todos (title) VALUES (?)",title).execute(&mut *conn).await?.last_insert_rowid();let todo = sqlx::query_as!(Todo, "SELECT * FROM todos WHERE id = ?", id).fetch_one(&mut *conn).await?;Ok(todo)}async fn get_all_todos_db(pool: &SqlitePool) -> Result<Vec<Todo>, SqlxError> {let todos = sqlx::query_as!(Todo, "SELECT * FROM todos").fetch_all(pool).await?;Ok(todos)}async fn get_todo_by_id_db(pool: &SqlitePool, id: i64) -> Result<Todo, SqlxError> {let todo = sqlx::query_as!(Todo, "SELECT * FROM todos WHERE id = ?", id).fetch_one(pool).await?;Ok(todo)}async fn update_todo_db(pool: &SqlitePool, id: i64, title: Option<String>, completed: Option<bool>) -> Result<Todo, SqlxError> {let mut conn = pool.acquire().await?;let old_todo = get_todo_by_id_db(pool, id).await?;let new_title = title.unwrap_or(old_todo.title);let new_completed = completed.unwrap_or(old_todo.completed);sqlx::query!("UPDATE todos SET title = ?, completed = ? WHERE id = ?",new_title,new_completed,id).execute(&mut *conn).await?;get_todo_by_id_db(pool, id).await}async fn delete_todo_db(pool: &SqlitePool, id: i64) -> Result<(), SqlxError> {let mut conn = pool.acquire().await?;sqlx::query!("DELETE FROM todos WHERE id = ?", id).execute(&mut *conn).await?;Ok(())}async fn create_todo(State(pool): State<SqlitePool>,Json(payload): Json<CreateTodo>,) -> Result<Json<Todo>, AppError> {payload.validate()?;let todo = create_todo_db(&pool, &payload.title).await?;Ok(Json(todo))}async fn get_all_todos(State(pool): State<SqlitePool>,) -> Result<Json<Vec<Todo>>, AppError> {let todos = get_all_todos_db(&pool).await?;Ok(Json(todos))}async fn get_todo_by_id(State(pool): State<SqlitePool>,Path(id): Path<i64>,) -> Result<Json<Todo>, AppError> {let todo = get_todo_by_id_db(&pool, id).await.map_err(|e| match e {SqlxError::RowNotFound => AppError::NotFound,_ => AppError::Sqlx(e)})?;Ok(Json(todo))}async fn update_todo(State(pool): State<SqlitePool>,Path(id): Path<i64>,Json(payload): Json<UpdateTodo>,) -> Result<Json<Todo>, AppError> {payload.validate()?;let todo = update_todo_db(&pool, id, payload.title, payload.completed).await.map_err(|e| match e {SqlxError::RowNotFound => AppError::NotFound,_ => AppError::Sqlx(e)})?;Ok(Json(todo))}async fn delete_todo(State(pool): State<SqlitePool>,Path(id): Path<i64>,) -> Result<StatusCode, AppError> {delete_todo_db(&pool, id).await.map_err(|e| match e {SqlxError::RowNotFound => AppError::NotFound,_ => AppError::Sqlx(e)})?;Ok(StatusCode::NO_CONTENT)}#[tokio::main]async fn main() {dotenv::dotenv().ok();let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");let pool = SqlitePool::connect(&database_url).await.expect("Failed to create pool.");let app = Router::new().route("/todos", get(get_all_todos).post(create_todo)).route("/todos/:id", get(get_todo_by_id).patch(update_todo).delete(delete_todo)).with_state(pool);let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();println!("Listening on {}", listener.local_addr().unwrap());axum::serve(listener, app).await.unwrap();}
第四部分:运行与测试
4.1 运行API服务
在项目根目录下运行:
cargo run
如果一切顺利,您将看到服务在0.0.0.0:3000上启动。

警告忽略即可:存在一个关于“未使用导入”(unused imports)的警告,不用当回事
4.2 使用cURL进行测试
创建一个新的Todo
curl -X POST -H "Content-Type: application/json" -d "{\"title\": \"学习 Rust\"}" http://localhost:3000/todos
获取所有Todo
curl http://localhost:3000/todos

总结
通过本文的实战项目,你学习到了如何使用Axum和SQLx构建一个高性能、类型安全的RESTful API。我们涵盖了从项目初始化、数据库迁移到实现CRUD操作、错误处理和数据验证的全过程。
使用Rust及其生态系统中的优秀库(如Axum和SQLx)来构建Web API,具有以下显著优势:
- 性能卓越:Rust的编译时优化和对底层硬件的精细控制,使其构建的应用具有极高的运行效率和低延迟,非常适合高并发场景。
- 内存安全:Rust的所有权和借用检查机制从根本上杜绝了空指针、悬垂指针等常见的内存安全问题,让您无需担心因内存管理不当而引发的运行时崩溃。
- 可靠性高:强大的类型系统和错误处理机制(如
Result和Option)使得代码更加健壮,能够在编译阶段就发现潜在的逻辑错误。 - 现代化的异步生态:基于Tokio的异步运行时,使得处理大量并发连接变得简单高效,能够充分利用多核处理器的性能。
这只是一个开始。Rust在Web开发领域的生态系统正在迅速成熟,Axum和SQLx是其中的佼佼者。希望本文能激发您使用Rust构建更多强大应用的兴趣。
想了解更多关于Rust语言的知识及应用,可前往华为开放原子旋武开源社区(https://xuanwu.openatom.cn/),了解更多资讯~
