使用 Rust 和 Axum 构建高性能 REST API

创建于 2024年4月18日修改于 2024年7月21日
RustAxum

在本教程中,我们将探讨如何使用Rust和Axum创建一个功能完备的REST API。我们将介绍设置路由和API端点、处理API请求、body和动态URL值、将API连接到MySQL数据库以及中间件集成的方法。

Contents

先决条件

要跟随本教程,需要具备以下先决条件:

什么是Axum?

Axum 是一个专注于性能和简单性的Web框架。它利用了 hyper 库的功能来增强Web应用程序的速度和并发性。Axum 还通过与 Tokio 库集成,将Rust的 async/await 功能推到了前台,使得开发者可以开发高性能的异步API和Web应用程序。

Axum的基本功能是基于 Tokio runtime 的,这给了Rust管理非阻塞、事件驱动活动的能力。这种能力对于平稳处理多个并发进程至关重要。

此外,Axum是基于Rust强大的类型系统和所有权规则构建的,这些规则在编译时防止了常见的Web开发陷阱,如数据竞争和内存泄漏。此外,Axum的模块化设计理念允许开发人员通过仅添加必要的组件来创建轻量级、专注的应用程序。

创建一个新的 Rust 应用程序

首先,让我们通过运行以下命令创建一个新的Rust应用程序。

cargo new my_rest_api
cd my_rest_api

这些命令为我们生成了一个新的 Rust 应用程序;它创建了一个新的 cargo.toml 文件,我们可以在其中管理我们的应用程序依赖项,以及一个新的 src/main.rs 文件,其中包含一个在控制台中输出 “hello world” 的Rust函数。

安装Axum、Tokio和Serde

下一步是安装我们应用程序的必要依赖项。我们将同时安装Serde和Tokio以及Axum。由于Rust缺乏用于处理JSON格式的内置函数,Serde将用于对JSON数据进行序列化和反序列化。由于Rust也缺乏原生异步运行时,安装的Tokio将用于提供异步运行时。

打开 cargo.toml 并使用以下配置更新依赖项部分。

. . .
[dependencies]
axum = {version = "0.6.20", features = ["headers"]}
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.68"
tokio = { version = "1.0", features = ["full"] }

接下来,通过运行以下命令安装依赖项:

cargo build

运行此命令会从 Crates.io 下载软件包到你的项目中。如果你之前使用过JavaScript和npm,这相当于向你的 package.json 文件中添加软件包并运行 npm install 来安装它们。

Hello, Rust!

现在我们已经安装了所有必要的软件包,让我们来尝试设置一个端点。打开默认的 src/main.rs 文件,并用以下代码替换它:

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(|| async { "Hello, Rust!" }));

    println!("Running on http://localhost:3000");
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

在上面的代码中的第一行,我们导入了Axum Router,并使用了它的get()路由方法。然后,我们利用了 #[tokio::main] 行将 main() 函数绑定到Tokio的运行时,使其异步。之后,我们定义了一个默认路由,以在GET查询中响应“Hello, Rust!”,并将其设置为在端口3000上监听。因此它现在可以在 http://localhost:3000 上访问。

要启动应用程序,请在终端中运行以下命令:

cargo run

运行此命令后,你应该会在终端中看到输出“Running on http://localhost:3000”。在浏览器中访问 http://localhost:3000 ,或使用类似 curl 的工具,将显示消息“Hello, Rust!”。

Axum 基础知识

路由和 handler

在Axum中,路由机制负责将传入的HTTP请求定向到其指定的 handler。这些 handler 实质上是包含处理请求逻辑的函数。

也就是说当我们定义一个新的端点时,我们也定义了处理传入到该端点的请求的函数;在Axum的语境下,这些函数称为 handler(处理器)。

路由器对象在这个过程中扮演了关键角色,因为它将URL映射到处理函数,并指定将接受端点的HTTP方法。下面的示例进一步阐述了这个概念。

打开你的 src/main.rs 文件,并用下面的代码替换其内容。

use axum::{
    body::Body,
    http::StatusCode,
    response::{IntoResponse, Response},
    routing::{get, post},
    Json, Router,
};
use serde::Serialize;

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}

// Handler for /create-user
async fn create_user() -> impl IntoResponse {
    Response::builder()
        .status(StatusCode::CREATED)
        .body(Body::from("User created successfully"))
        .unwrap()
}
// Handler for /users
async fn list_users() -> Json<Vec<User>> {
    let users = vec![
        User {
            id: 1,
            name: "Elijah".to_string(),
            email: "elijah@example.com".to_string(),
        },
        User {
            id: 2,
            name: "John".to_string(),
            email: "john@doe.com".to_string(),
        },
    ];
    Json(users)
}

#[tokio::main]
async fn main() {
    // Define Routes
    let app = Router::new()
        .route("/", get(|| async { "Hello, Rust!" }))
        .route("/create-user", post(create_user))
        .route("/users", get(list_users));

    println!("Running on http://localhost:3000");
    // Start Server
    axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

上面的代码演示了Axum路由和处理器的工作原理。我们使用 Router::new() 定义了应用程序的路由,指定了HTTP方法GET、POST、PUT、DELETE等,以及它们对应的处理器函数。

/users 路由为例;它被定义为处理GET请求,并将 list_users() 函数设置为其处理器。这是一个异步函数,基本上返回了两个预定义用户的JSON数组。此外,由于 Rust 原生不支持JSON格式,User 结构启用了 Serde 的 Serialize 特性,以允许将用户实例转换为JSON。

另一方面,/create-user 路由接受POST请求,并将其处理器设置为 create_user() 函数。这个函数通过 status(StatusCode::CREATED) 返回一个201状态码,以及一个静态响应,“用户创建成功”。

要尝试效果,重新启动代码,并使用以下 curl 命令发送POST请求到 /create-user 端点。

curl -X POST http://localhost:3000/create-user

你应该会看到消息“User created successfully”。

或者,访问浏览器中的 /users,你应该会看到我们定义的静态用户列表。

提取器

Axum中的提取器是一个强大的功能,它解析和转换传入HTTP请求的部分,将其转换为处理程序函数中的类型化数据。它们使你可以轻松地以一种类型安全的方式访问请求参数,例如路径段、查询字符串和 body。

使用 path 和 query 提取器的GET请求

例如,要捕获动态URL值以及查询字符串,我们可以轻松地在处理程序函数中指定它们。更新你的 src/main.rs 文件,使用下面的代码看看它的效果。

use axum::{
    extract::{Path, Query},
    routing::get,
    Router,
};
use serde::Deserialize;

// A struct for query parameters
#[derive(Deserialize)]
struct Page {
    number: u32,
}

// A handler to demonstrate path and query extractors
async fn show_item(Path(id): Path<u32>, Query(page): Query<Page>) -> String {
    format!("Item {} on page {}", id, page.number)
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/item/:id", get(show_item));
    axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

在这个例子中,我们可以定义一个动态URL,即 /path/:id 模式。这是其他语言中也很常见的语法。此外,show_item() 处理程序使用 path 提取器从URL中获取项目的ID,并使用 query 提取器从查询字符串中获取页码。当向此端点发出请求时,Axum会负责调用正确的处理程序并提供提取的数据作为参数。

尝试一下,重新启动应用程序,然后运行以下 curl 命令:

curl "http://localhost:3000/item/42?number=2"

你应该会在终端上看到“item 42 on page 2”的输出。

使用 JSON body 提取器的POST请求

对于POST请求,通常需要处理发送到请求 body 中的数据,Axum提供了JSON提取器,将JSON数据解析为Rust类型。使用下面的代码更新 src/main.rs

use axum::{extract::Json, routing::post, Router};
use serde::Deserialize;

// A struct for the JSON body
#[derive(Deserialize)]
struct Item {
    title: String,
}

// A handler to demonstrate the JSON body extractor
async fn add_item(Json(item): Json<Item>) -> String {
    format!("Added item: {}", item.title)
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/add-item", post(add_item));
    axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

在上面的示例中,我们定义了一个新的 /add-item 端点,它接受POST请求。在其处理程序函数 add_item() 中,我们使用JSON提取器将传入的JSON主体解析为 Item 结构。这展示了Axum在解析传入请求主体方面的简单性。

你可以通过重新启动应用程序并运行以下命令来尝试此示例:

curl -X POST http://localhost:3000/add-item \
    -H "Content-Type: application/json" \
    -d '{"title": "Some random item"}'

一旦执行,我们应该会得到响应“Added item: Some random item”。

错误处理

Axum提供了一种统一处理应用程序中的错误的方法。处理程序可以返回 Result 类型,用于优雅地处理错误并返回适当的HTTP响应。

下面是在处理程序函数中处理错误的示例。要查看它在实际中的应用,请使用下面的代码更新 src/main.rs 并重新启动你的应用。

use axum::{
    extract::Path, http::StatusCode, response::IntoResponse, routing::delete, Json, Router,
};
use serde::Serialize;

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
}

// Define a handler that performs an operation and may return an error
async fn delete_user(Path(user_id): Path<u64>) -> Result<Json<User>, impl IntoResponse> {
    match perform_delete_user(user_id).await {
        Ok(_) => Ok(Json(User {
            id: user_id,
            name: "Deleted User".into(),
        })),
        Err(e) => Err((
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Failed to delete user: {}", e),
        )),
    }
}

// Hypothetical async function to delete a user by ID
async fn perform_delete_user(user_id: u64) -> Result<(), String> {
    // Simulate an error for demonstration
    if user_id == 1 {
        Err("User cannot be deleted.".to_string())
    } else {
        // Logic to delete a user...
        Ok(())
    }
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/delete-user/:user_id", delete(delete_user));

    println!("Running on http://localhost:3000");
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

在上面的示例中,我们定义了一个 /delete-user/:user_id 路由,用于模拟删除给定 user_id 的用户。在其处理程序函数 delete_user 中,我们尝试使用另一个模拟的 perform_delete_user() 函数删除用户。如果成功,我们将使用虚拟用户JSON响应返回Ok变体。如果出现错误,我们将使用 HTTP 500 (Internal Server Error) 状态和错误消息返回Err变体。

你可以使用以下 curl 命令测试 /delete-user 端点:

curl -X DELETE http://localhost:3000/delete-user/1

该命令向 /delete-user 端点发送了一个带有ID为1的用户的删除请求。根据提供的代码,这应该触发错误条件并返回错误响应。如果要测试成功删除的场景,请用任何其他数字替换1。例如:

curl -X DELETE http://localhost:3000/delete-user/2

这应该模拟成功删除并返回成功响应。

Axum 中的高级技术

现在我们已经涵盖了Axum的基本知识,让我们探讨一些构建强大API所必需的附加功能。

数据库集成

集成数据库是API开发中的关键步骤。幸运的是,Axum与任何异步Rust数据库库无缝配合。在本示例中,我们将使用 sqlx crate 集成MySQL数据库,该crate支持async/await,并与Axum的异步特性兼容。

继续之前,请确保你的MySQL服务正在后台运行。接下来,通过向 Cargo.toml 文件添加下面的依赖项以及Tokio运行时来实现SQLx和相应的MySQL特性。

sqlx = { version = "0.7.2", features = ["runtime-tokio", "mysql"] }

然后,运行以下命令来获取新的依赖项:

cargo build

设置好后,你现在可以使用 MySqlPool::connect() 方法建立与你的MySQL数据库的连接池。如下所示,替换 database_url 中的占位符。

use axum::{routing::get, Router};
use sqlx::MySqlPool;

#[tokio::main]
async fn main() {
    let database_url = "mysql://<<USERNAME>>:<<PASSWORD>>@<<HOSTNAME>>/<<DATABASE NAME>>";
    let pool = MySqlPool::connect(&database_url)
        .await
        .expect("Could not connect to the database");

    let app = Router::new().route("/", get(|| async { "Hello, Rust!" }));

    println!("Running on http://localhost:3000");
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

连接池准备就绪后,你现在可以在函数中执行数据库查询,使用以下语法:

async fn fetch_data(pool: MySqlPool) -> Result<Json<MyDataType>, sqlx::Error> {
    let data = sqlx::query_as!(MyDataType, "SELECT * FROM my_table")
        .fetch_all(&pool)
        .await?;
    
    Ok(Json(data))
}

由于我们正在与Axum集成,我们的处理程序函数需要以 Extension<MySqlPool> 作为参数;这允许Axum在请求到达我们的端点时向处理程序函数提供MySqlPool。

假设,你希望你的端点返回MySQL数据库中的所有用户。首先,在你的MySQL数据库中创建一个名为users的新表,其结构如下所示。

create table users (
    id int primary key auto_increment,
    name varchar(200) not null,
    email varchar(200) not null
);

然后,运行以下命令向此表添加新条目。

INSERT INTO users (id, name, email) 
VALUES (1, 'Alice Smith', 'alice.smith@example.com'), 
(2, 'Bob Johnson', 'bob.johnson@example.com'), 
(3, 'Charlie Lee', 'charlie.lee@example.com'), 
(4, 'Dana White', 'dana.white@example.com'), 
(5, 'Evan Brown', 'evan.brown@example.com');

设置好表格后,通常会这样设置 Axum 与 SQLx 的协作。

use axum::{extract::Extension, response::IntoResponse, routing::get, Json, Router, Server};
use serde_json::json;
use sqlx::{MySqlPool, Row};

// Define the get_users function as before
async fn get_users(Extension(pool): Extension<MySqlPool>) -> impl IntoResponse {
    let rows = match sqlx::query("SELECT id, name, email FROM users")
        .fetch_all(&pool)
        .await
    {
        Ok(rows) => rows,
        Err(_) => {
            return (
                axum::http::StatusCode::INTERNAL_SERVER_ERROR,
                "Internal server error",
            )
                .into_response()
        }
    };

    let users: Vec<serde_json::Value> = rows
        .into_iter()
        .map(|row| {
            json!({
                "id": row.try_get::<i32, _>("id").unwrap_or_default(),
                "name": row.try_get::<String, _>("name").unwrap_or_default(),
                "email": row.try_get::<String, _>("email").unwrap_or_default(),
            })
        })
        .collect();

    (axum::http::StatusCode::OK, Json(users)).into_response()
}

#[tokio::main]
async fn main() {
    // Set up the database connection pool
    let database_url = "mysql://<<USERNAME>>:<<PASSWORD>>@<<HOSTNAME>>/<<DATABASE_NAME>>";
    let pool = MySqlPool::connect(&database_url)
        .await
        .expect("Could not connect to the database");

    // Create the Axum router
    let app = Router::new()
        .route("/users", get(get_users))
        .layer(Extension(pool));

    // Run the Axum server
    Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

更新 src/main.rs 代码后重启。在浏览器中打开 http://localhost:3000/users ,你将看到存在数据库中的数据列表。

中间件

Axum的中间件功能允许你在请求到达处理程序之前或之后添加自定义逻辑。这为通用功能的实现提供了强大的方法,例如身份验证、日志记录或性能监视。

例如,以下是一个简单的记录请求路径的中间件的示例:

use axum::{
    body::Body,
    http::Request,
    middleware::{self, Next},
    response::Response,
    routing::get,
    Router, Server,
};

async fn logging_middleware(req: Request<Body>, next: Next<Body>) -> Response {
    println!("Received a request to {}", req.uri());
    next.run(req).await
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, world!" }))
        .layer(middleware::from_fn(logging_middleware));

    Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

现在,用户访问任意端点都会被终端记录下来,如下所示:

Received a request to /users
Received a request to /
Received a request to /test

总结

在本文中,我们探讨了使用Rust和Axum构建高性能REST API的过程。我们深入了解了该框架的强大功能,从路由到错误处理,还触及了数据库集成和中间件等高级主题。

感谢阅读!

原文链接:https://www.twilio.com/en-us/blog/build-high-performance-rest-apis-rust-axum#Axum-basics