cd my_rest_api

執行這些命令后,我們將獲得一個新的 Rust 應用程序框架;

這個框架包括一個 cargo.toml 文件,用于管理應用程序的依賴項,以及一個 src/main.rs 文件,其中包含一個 Rust 函數,能夠在控制臺輸出 “hello world”。

安裝所需依賴項

接下來,我們需要為應用程序安裝必要的依賴項。在本教程中,我們將安裝 Axum、Serde 和 Tokio。Serde 用于 JSON 數據的序列化和反序列化,因為 Rust 本身不直接支持 JSON 格式的處理。Tokio 則提供異步運行時支持,這也是 Rust 標準庫所不具備的功能。

為了進行這些安裝,請打開 cargo.toml 文件,并在 dependencies 部分添加以下配置。

. . .
[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(Rust 的包注冊表)下載所需的包到您的項目中。如果您有 JavaScript 和 npm 的使用經驗,這個過程相當于在 package.json 文件中添加依賴項,然后運行 npm install 來安裝它們。

你好,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” 的輸出信息。此時,您可以在瀏覽器中訪問該地址,或者使用 curl 等工具,來查看顯示的 “Hello, Rust!” 消息。

Axum 基礎知識

路由和處理程序

在 Axum 框架中,路由機制扮演著將接收到的 HTTP 請求引導至相應處理程序的重要角色。這些處理程序本質上就是包含請求處理邏輯的函數。

簡而言之,每當我們定義一個新的端點時,其實也在定義一個用于處理該端點接收到的請求的函數,在 Axum 中,這些函數被稱為處理程序。

在此過程中,router 對象發揮著至關重要的作用,因為它負責將特定的 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等)與它們各自對應的處理函數。

/users路由為例,它被設定為響應GET請求,而list_users()函數則作為該請求的處理程序。這個函數是一個簡單的JSON響應函數,它會返回一個包含兩個預定義用戶的JSON數組。由于Rust原生并不直接支持JSON格式,因此我們需要為User結構體實現Serde的Serialize trait,以便能夠將User實例轉換為JSON格式。

另一方面,/create-user路由則設計為接受POST請求,其處理程序是create_user()函數。這個函數通過status(StatusCode::CREATED)返回了一個201狀態碼,并附帶了一個靜態響應:“User created successfully”。

想要實際體驗一下這些功能嗎?那就重新啟動你的代碼,并使用以下curl命令向/create-user端點發送一個POST請求吧!

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

您應該看到消息“用戶創建成功”。

另外,如果您在瀏覽器中訪問/users路徑,應該會看到一個顯示我們預先定義的靜態用戶列表的頁面,內容大致如下。

在瀏覽器中以 JSON 格式呈現的靜態用戶列表。

Axum 提取器

Axum 框架中的提取器(Extractor)是一項極為強大的功能,它能夠解析傳入的 HTTP 請求中的部分內容,并將其轉換為處理程序函數所能直接使用的類型化數據。通過這一機制,開發者能夠以類型安全的方式便捷地訪問請求中的各類參數,如路徑段(Path Segments)、查詢字符串(Query Strings)以及請求正文(Request Body)等。

帶有路徑和查詢提取器的 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();
}

在此示例中,我們利用/path/:id這樣的模式定義了一個包含動態部分的URL。這種語法在其他編程語言中也頗為常見。此外,show_item()處理程序通過路徑提取器從URL中捕獲了項目ID,并利用查詢提取器從查詢字符串中獲取了頁碼信息。當向該端點發送請求時,Axum框架會自動調用相應的處理程序,并將已提取的數據作為參數傳遞給該處理程序。

接下來,您可以嘗試重新啟動應用程序,并通過運行以下curl命令來驗證這一功能:

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

執行上述curl命令后,您應該會在終端中看到“第 2 頁第 42 項”的輸出。

使用 JSON 正文提取器的 POST 請求

在處理 POST 請求時,經常需要解析請求正文中發送的數據。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"}'

一旦執行上述命令,我們應該會收到一條響應消息:“添加的項目:一些隨機項目”。

錯誤處理

Axum框架為開發者提供了一種在應用層面統一處理錯誤的有效途徑。具體而言,處理程序(Handler)可以返回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 狀態碼(內部服務器錯誤)及錯誤信息的 Err 值。

為了測試這個 /delete-user 端點,您可以使用以下 curl 命令:

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

上述命令會向用戶ID為1的/delete-user端點發送一個刪除請求。根據示例代碼中的邏輯,這通常會觸發錯誤條件,并導致服務器返回一個錯誤響應,然而,如果您想要測試成功刪除的場景,只需將用戶ID?1?替換為任意其他數字即可。例如:

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

執行上述命令將模擬一個成功的刪除操作,并促使服務器返回一個表示操作成功的響應。通過這種方式,您可以驗證/delete-user端點在不同情況下的行為是否符合預期。

Axum 中的高級技術

在掌握了 Axum 的基礎知識之后,讓我們一同深入挖掘那些對于構建功能強大的 API 至關重要的其他高級功能。

數據庫集成

在 API 開發過程中,數據庫的集成是至關重要的一環。幸運的是,Axum 能夠與任何異步 Rust 數據庫庫實現無縫對接。在此,我們以 sqlx crate 為例,展示如何將其與 MySQL 數據庫進行集成。sqlx 是一個支持 async/await 的數據庫庫,與 Axum 的異步特性完美契合。

要開始進行集成工作,請確保您的 MySQL 服務已在后臺正常運行。接下來,您需要在?Cargo.toml?文件中添加?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();
}

在這個升級后的示例中,我們對應用程序的路由器進行了更新,增加了一個新的擴展定義,用于傳入 MySQL 連接池,得益于這一改動,在處理程序函數中,我們現在可以輕松地訪問到這個連接池。

處理程序函數會執行一個 SQL 查詢,從數據庫中檢索用戶信息,并根據查詢結果提取用戶的 idname 和 email 字段。隨后,這些信息會被封裝在響應體中,通過 API 終端節點返回給客戶端。

在將 src/main.rs 文件中的代碼替換為上述示例后,您可以啟動應用程序,并在瀏覽器中訪問 http://localhost:3000/users。此時,您應該會看到一個包含用戶信息的 JSON 數組,具體內容取決于您數據庫中存儲的數據。

在瀏覽器中以 JSON 格式呈現的靜態用戶列表。

中間件

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
Received a request to /todos

提升 REST API 性能的建議

Rust 語言所具備的獨特并發處理機制、零成本抽象以及強大的類型系統,為高性能 API 的開發奠定了堅實的基礎。而在使用 Axum 框架時,這些優勢更是得到了進一步的發揮。

然而,要想讓 API 的性能更上一層樓,我們還需要深入理解并靈活運用 Rust 的所有權和借用原則,從而實現對內存的有效管理。此外,減少共享資源的鎖競爭、精心選擇序列化方法以避免性能瓶頸,也是提升性能的關鍵。在選擇序列化格式和庫時,我們應優先考慮效率和性能,正如本文示例中所展示的那樣。

但僅編寫高效的代碼是不夠的,我們還需要借助如 Criterion 等性能分析工具,對代碼進行定期的性能評估。通過這些工具,我們可以及時發現并解決潛在的性能瓶頸。例如,利用 Criterion 對關鍵函數進行基準測試,就是一種非常有效的性能調優手段。

use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn process_data(data: &[u8]) -> usize {
// Simulate data processing
data.len()
}

fn benchmark(c: &mut Criterion) {
c.bench_function("process_data", |b| {
b.iter(|| process_data(black_box(&[1, 2, 3, 4, 5])))
});
}

criterion_group!(benches, benchmark);
criterion_main!(benches);

為了持續享受 Rust 和 Axum 帶來的性能優勢,保持 Rust 編譯器及其依賴項的更新至關重要。這樣,您的項目就能不斷從最新的性能優化和特性改進中獲益。

總結:構建高性能 REST API 的 Rust 與 Axum 之道

本文全面探討了利用 Rust 和 Axum 構建高性能 REST API 的精髓。我們從框架的核心功能出發,逐步深入到路由處理、錯誤管理、數據庫集成以及中間件等進階話題。同時,我們還分享了一系列提升 API 性能的有效策略。

感謝閱讀!

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

上一篇:

NestJ框架下的RESTful API簡介

下一篇:

如何使用Rust構建API服務器
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費