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

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

1.2 添加依赖
接下来,在Cargo.toml文件中添加项目所需的依赖:
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文件夹,用于存放数据库迁移文件:
bash
md -p .sqlx/migrations

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

这会在.sqlx/migrations目录下生成一个SQL文件。编辑该文件,定义todos表的结构:
sql
-- .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
);
现在,运行迁移以创建数据库和表:
bash
sqlx migrate run

第二部分:构建核心业务逻辑
2.1 定义数据模型
在src/main.rs中,我们首先定义Todo模型和用于创建、更新Todo项的数据传输对象(DTO)。
rust
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 实现数据库操作
接下来,我们创建数据库连接池,并实现与数据库交互的函数。
rust
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 错误处理
为了提供清晰的错误信息,我们定义一个自定义的错误类型。
rust
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路由,并将它们与相应的处理器函数关联起来。
rust
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
rust
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
rust
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错误。
程序完整代码如下
rust
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服务
在项目根目录下运行:
bash
cargo run
如果一切顺利,您将看到服务在0.0.0.0:3000上启动。

警告忽略即可:存在一个关于"未使用导入"(unused imports)的警告,不用当回事
4.2 使用cURL进行测试
创建一个新的Todo
bash
curl -X POST -H "Content-Type: application/json" -d "{\"title\": \"学习 Rust\"}" http://localhost:3000/todos
获取所有Todo
bash
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/),了解更多资讯~