所有文章 > API开发 > 如何使用Rust构建API服务器
如何使用Rust构建API服务器

如何使用Rust构建API服务器

使用 Actix 和 MongoDB 构建可扩展的 Rust HTTP 服务器的分步教程。

介绍

在本文中,我们将探索如何使用 Rust 语言结合 Actix 框架来构建一个 Web API 服务器,并选用 MongoDB 作为数据库。

为什么选择 Rust?

在构建现代 API 服务器的诸多选项中,Web 社区对 TypeScript、Go 和 Rust 这三种语言尤为关注。其中,TypeScript 以其高效的生产力著称,但在性能方面略显不足;Rust 则以卓越的性能见长,尽管在生产力方面可能稍逊一筹;而 Go 则在两者之间取得了平衡。

作为 TypeScript 的日常使用者,我深知其带来的高效生产力。然而,我也清楚地认识到这门语言的局限性。那么,为何还要选择 Rust 呢?

类型安全

在 TypeScript 中构建 REST API 时,我们可能会轻易忽视一些细节,比如为预期的请求体或第三方数据源提供详尽的类型定义。而 Rust 则截然不同,它要求我们为任何数据类型提供完整的数据结构,或者有意识地选择不承担这一责任(例如,通过使用类似 serde_json 库中的 Value 类型)。

执行速度

Rust 还带来了其他显著优势。它的执行速度极快,几乎可以媲美 C 和 C++。

由于 Rust 的高速执行能力和内存安全性,再加上无需承担垃圾回收的开销,垂直扩展 Rust 程序通常比 Node.js 程序更为高效。这有助于降低基础设施的成本和复杂性。

在撰写本文时,Rust 已成为本基准测试中排名前 20 位的 HTTP 框架最常用的语言之一。而在这些框架中,我们将选用 Actix。Actix 以其出色的性能和在 Rust 社区中的受欢迎程度而著称。将 Actix 的速度与 Rust 的数据处理能力相结合,我们有望打造出一个极其快速的 API。

生产力考量

除了探索如何用 Rust 编写 API 之外,我还想解答另一个关键问题:在使用 Rust 时,是否会感觉生产力不如 TypeScript 高?这是一个难以量化的比较,因为我正在将我极为熟悉的语言(TypeScript)与仍在深入学习的语言(Rust)进行对比。

在构建这个项目的过程中,我并未觉得 Rust 的开发速度比 TypeScript 慢太多。诚然,设置 Actix 的过程比配置 Express.js 之类的框架要复杂一些。一般来说,Rust 中的异步编程确实比 TypeScript 中的异步编程更具挑战性。

然而,令人惊讶的是,在某些方面,在 Rust 中使用 MongoDB 反而感觉比在 TypeScript 中更容易。这是因为无需像 Mongoose 和 Typegoose 这样的额外库来辅助开发。当然,处理从请求格式到数据库格式的转换确实增加了额外的复杂性。总的来说,在 Rust 实现中需要考虑的事项确实更多。但是,如果我们的代码因此减少了维护成本,那么从长远来看,生产力的提升或许会成为现实!

为什么选择 MongoDB?

我选择 MongoDB 作为我们的数据库,主要是因为它相较于主要的 SQL 替代方案(如 PostgreSQL)来说,设置过程更为迅速。我们希望能够专注于 Rust 的开发,而不是花费大量时间在数据库的设置上!

同时,我认为 MongoDB 与 Rust 是很好的搭配,Rust 严格的类型系统能够为 MongoDB 的无模式设计带来安全性和可预测性。我们可以用 Rust 编写类型定义,而无需在数据库层面进行额外的努力。

如果您想继续操作并需要手动设置 MongoDB,请查阅官方的安装指南。

我们正在构建什么

我们计划创建一个 API,旨在帮助管理遛狗预订。说起来,这个项目的起因是我女朋友最近开展了遛狗业务,因此,她急需一个 Rust Web 服务器来支持她的业务。没错,我就是这么浪漫的人。

当然,对于这项特定任务来说,Rust 可能显得有些大材小用。但我的主要目的是展示如何为 REST API 搭建一个基础且可扩展的架构。

您可以在此处找到该项目的最终成品。

在深入代码之前,让我们先规划一下要存储的数据类型以及核心功能。

我们需要存储以下三种主要类型的数据:

  • 狗:我们需要知道每只宠物的名字、年龄和品种。
  • 所有者:我们需要了解每个所有者的姓名、地址和一些联系方式。
  • 预订:我们需要知道每个预订的开始时间、持续时间以及预订的所有者。

那么,主要功能有哪些呢?在第一轮开发中,我们会保持简洁,并专注于以下几个端点:

  • POST /owner:用于添加新的所有者。
  • POST /dog:为指定的所有者添加一只狗。
  • POST /booking:为指定的所有者添加预订。
  • GET /bookings:获取所有未来的预订,按时间从近到远排序。
  • PUT /booking/{id}/cancel:取消未完成的特定预订。

这些功能应该足够满足我们当前的需求。一旦预订量开始增加,我们就可以添加新功能,例如更新或删除记录!

现在,让我们开始编程吧。

安装 Rust

要继续进行后续操作,请确保您的系统已经安装了 Rust 和 Cargo。您可以通过运行以下命令来检查它们是否已安装:

rustc --version
cargo --version

如果未安装 Rust,请按照 Rust 官方网站上的说明进行操作。

创建新项目

让我们从使用 cargo 来搭建新项目的基架开始:

cargo new rust-web-server-tutorial

然后,打开并添加以下依赖项:

[dependencies]
actix-web = "4"
chrono = "0.4.37"
futures-util = "0.3.30"
mongodb = "2.8.2"
serde = "1.0.197"

我们已经讨论了选择 Actix 和 MongoDB 的原因。接下来,让我们看看其他依赖项的作用:

  • 我们将使用 chrono crate 来将字符串解析为日期格式。
  • futures-util crate 将帮助我们处理 MongoDB 的 Cursor 结构,它实现了 Stream trait,并在我们获取多个文档时派上用场。
  • 至于 serde,这是一个非常流行的 crate,用于序列化和反序列化 Rust 数据结构。它允许我们将 Rust 结构转换为可以通过网络发送的格式(如 JSON),并将来自网络的数据转换为我们的 Rust 数据结构。

现在,让我们在入口点文件 main.rs 中添加一些新的模板代码,以便 main 函数能够返回一个 Actix HTTP 服务器,并将其绑定到 http://localhost:5001

use actix_web::{get, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello Medium!")
}

#[actix_web::main]
async fn main() -> std::io::Result() {
HttpServer::new(|| App::new().service(hello))
.bind(("localhost", 5001))?
.run()
.await
}

在这里,您将看到我们正在利用一个路由来运行 HTTP 服务器。我们借助 Actix 提供的宏来定义路由的方法及其对应的路径。在本例中,我们使用了 get 方法和根路径 "/"

最后,让我们来运行这个项目。在开发 Rust 项目时,我通常喜欢使用以下命令:

cargo watch -c -w src -x run

以下是配置标志的作用:

  • -c 或 --clear 标志用于在每次更改之间清除屏幕,以保持输出的整洁。
  • -w src 或 --watch src 告诉 cargo watch 只关注 src 目录中的文件变化。
  • -x run 表示 watch 命令仅在检测到文件更改时运行 cargo run

现在,如果您访问 http://localhost:5001,应该能看到文本 “Hello Medium!”。

测试和调试

如果您是 Rust 的新手,可能会发现满足编译器要求需要比使用大多数其他语言更多的时间。但请放心,这通常会带来更加稳健的代码!

在开发过程中,如果您想运行某个特定的函数,可以将其导入到 main.rs 中,并在 main 函数中调用它。为了方便调试,我已经在所有的数据结构中实现了 Debug trait,因此您可以使用 dbg! 宏来查看大多数变量的内容。

您可以使用 cURL、Postman、Insomnia 等工具来测试 API 终端节点。在本文末尾,我提供了一些示例 cURL 命令供您参考。

设置文件系统

我们将把我们的计划分为三个主要领域:

  • models:用于定义在数据库中使用的数据结构以及处理 HTTP 请求和响应的数据结构。
  • routes:在这里,我们定义 API 的端点、方法和路径,并负责将请求的 JSON 数据转换为我们的数据结构。
  • services:此部分包含初始化数据库的代码以及与数据库交互的逻辑。这些服务可以从我们的路由中调用。

按照这样的规划,我们的文件系统结构应如下所示。

rust-web-server-tutorial/
└── src/
├── main.rs
├── models/
│ ├── booking_model.rs
│ ├── dog_model.rs
│ ├── mod.rs
│ └── owner_model.rs
├── routes/
│ ├── booking_route.rs
│ ├── dog_route.rs
│ ├── mod.rs
│ └── owner_route.rs
└── services/
├── db.rs
└── mod.rs

要快速设置,请在项目根目录中运行以下 shell 命令:

cd src && \
mkdir models routes services && \
touch models/booking_model.rs models/dog_model.rs models/mod.rs models/owner_model.rs && \
touch routes/booking_route.rs routes/dog_route.rs routes/mod.rs routes/owner_route.rs && \
touch services/db.rs services/mod.rs && \
cd ..

定义我们的模型

首先,我们需要明确数据库中所需数据的结构,以及端点请求正文中将支持的内容格式。

预订模型

让我们从 Booking 模型开始设计。

// booking_model.rs
use mongodb::bson::{oid::ObjectId, DateTime};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct Booking {
pub _id: ObjectId,
pub owner: ObjectId,
pub start_time: DateTime,
pub duration_in_minutes: u8,
pub cancelled: bool,
}

这表示我们在数据库中希望存储的内容有一定的格式。但是,对于最终用户来说,我们期望他们通过HTTP请求正文以不同的方式提交数据。具体来说,用户在创建新预订时,我们不希望他们自行指定 _id 字段。此外,为了提升用户体验,我们希望简化数据类型,允许最终用户以字符串形式提交 start_time,并可以直接指定 owner

为了达到这一目的,我们将创建一个 BookingRequest 结构体,并编写一个从 BookingRequest 转换到 Booking 的逻辑,这一逻辑将通过实现 TryFrom<BookingRequest> trait 来完成。

// booking_model.rs
use std::{convert::TryFrom, time::SystemTime};

use chrono::Utc;
use mongodb::bson::{oid::ObjectId, DateTime};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct Booking {
pub _id: ObjectId,
pub owner: ObjectId,
pub start_time: DateTime,
pub duration_in_minutes: u8,
pub cancelled: bool,
}

#[derive(Debug, Deserialize)]
pub struct BookingRequest {
pub owner: String,
pub start_time: String,
pub duration_in_minutes: u8,
}

impl TryFromBookingRequest for Booking {
type Error = Boxdyn std::error::Error;

fn try_from(item: BookingRequest) -> ResultSelf, Self::Error {
let chrono_datetime: SystemTime = chrono::DateTime::parse_from_rfc3339(&item.start_time)
.map_err(|err| format!("Failed to parse start_time: {}", err))?
.with_timezone(&Utc)
.into();

Ok(Self {
_id: ObjectId::new(),
owner: ObjectId::parse_str(&item.owner).expect("Failed to parse owner"),
start_time: DateTime::from(chrono_datetime),
duration_in_minutes: item.duration_in_minutes,
cancelled: false,
})
}
}

确实,选择使用 TryFrom 而不是 From 是明智的,因为处理来自第三方的数据时,转换过程总是存在失败的可能性,而 From 转换要求总是能够成功。

关于 Box<dyn std::error::Error>,这是一个值得关注的错误类型。它是一个 trait 对象,能够存储任何实现了 std::error::Error trait 的类型。这种错误类型非常通用,能够表示多种不同类型的错误。在函数可能以多种方式失败,而我们又不想手动处理每个可能的失败情况时,它就显得特别有用。

我们的工作还远未结束!

展望未来,我们明白需要构建一个端点,用于返回包含所有相关主人和狗信息的预订详情。我们可以将这个新的数据结构命名为 FullBooking,并将其添加到我们的文件中。添加完这个新结构后,我们的整体设计将如下所示:

// booking_model.rs
use std::{convert::TryFrom, time::SystemTime};

use super::{dog_model::Dog, owner_model::Owner};
use chrono::Utc;
use mongodb::bson::{oid::ObjectId, DateTime};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct Booking {
pub _id: ObjectId,
pub owner: ObjectId,
pub start_time: DateTime,
pub duration_in_minutes: u8,
pub cancelled: bool,
}

#[derive(Debug, Deserialize)]
pub struct BookingRequest {
pub owner: String,
pub start_time: String,
pub duration_in_minutes: u8,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct FullBooking {
pub _id: ObjectId,
pub owner: Owner,
pub dogs: VecDog,
pub start_time: DateTime,
pub duration_in_minutes: u8,
pub cancelled: bool,
}

impl TryFromBookingRequest for Booking {
type Error = Boxdyn std::error::Error;

fn try_from(item: BookingRequest) -> ResultSelf, Self::Error {
let chrono_datetime: SystemTime = chrono::DateTime::parse_from_rfc3339(&item.start_time)
.map_err(|err| format!("Failed to parse start_time: {}", err))?
.with_timezone(&Utc)
.into();

Ok(Self {
_id: ObjectId::new(),
owner: ObjectId::parse_str(&item.owner).expect("Failed to parse owner"),
start_time: DateTime::from(chrono_datetime),
duration_in_minutes: item.duration_in_minutes,
cancelled: false,
})
}
}

既然我们已经了解了模式的设计思路,那么接下来就可以实现其他更简单的模型了。

接下来,让我们定义Owner模型。

所有者模型

考虑到我们的客户可能并不总是愿意提供电子邮件地址,因此我们将电子邮件字段包装在 Option 类型中,以表示该字段是可选的。然而,为了与客户保持联系,我们通常需要知道他们的住址和电话号码,因此这些字段是必需的。

此外,每个所有者都会有一个唯一的 ObjectId,在处理请求时,我们还需要考虑如何将从请求中接收到的类型转换为数据库中的类型。

基于以上考虑,我们可以设计出如下的 Owner 数据结构。

// owner_model.rs
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;

#[derive(Debug, Serialize, Deserialize)]
pub struct Owner {
pub _id: ObjectId,
pub name: String,
pub email: OptionString,
pub phone: String,
pub address: String,
}

#[derive(Debug, Deserialize)]
pub struct OwnerRequest {
pub name: String,
pub email: OptionString,
pub phone: String,
pub address: String,
}

impl TryFromOwnerRequest for Owner {
type Error = Boxdyn std::error::Error;

fn try_from(item: OwnerRequest) -> ResultSelf, Self::Error {
Ok(Self {
_id: ObjectId::new(),
name: item.name,
email: item.email,
phone: item.phone,
address: item.address,
})
}
}

Dog Model

最后,我们可以实现我们的Dog模型。

// dog_model.rs
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct Dog {
pub _id: ObjectId,
pub owner: ObjectId,
pub name: OptionString,
pub age: Optionu8,
pub breed: OptionString,
}

#[derive(Debug, Deserialize)]
pub struct DogRequest {
pub owner: String,
pub name: OptionString,
pub age: Optionu8,
pub breed: OptionString,
}

impl TryFromDogRequest for Dog {
type Error = Boxdyn std::error::Error;

fn try_from(item: DogRequest) -> ResultSelf, Self::Error {
Ok(Self {
_id: ObjectId::new(),
owner: ObjectId::parse_str(&item.owner).expect("Failed to parse owner"),
name: item.name,
age: item.age,
breed: item.breed,
})
}
}

不要忘记在mod.rs中公开这些模型,这将把我们的models文件夹变成一个模块,我们可以导入到其他地方。

// mod.rs
pub mod booking_model;
pub mod dog_model;
pub mod owner_model;

确保将mod models添加到main.rs文件的顶部,以便其他目录可以访问此模块。

添加数据库服务

为了集中处理数据库连接、集合操作以及相关的数据库方法,我们将在 services 文件夹下的 db.rs 文件中进行这些服务的定义。随着项目的不断发展,我们可能会考虑将这个文件拆分成多个更小的文件,但就目前而言,使用单个文件作为起点会更为简便。

定义和初始化我们的数据库

首先,让我们为out Database添加一个结构体。

// db.rs
use crate::models::booking_model::{Booking, FullBooking};
use crate::models::dog_model::Dog;
use crate::models::owner_model::Owner;

pub struct Database {
booking: CollectionBooking,
dog: CollectionDog,
owner: CollectionOwner,
}

接下来,我们将实现一个 init 方法,该方法会尝试使用 MONGO_URI 环境变量来连接到数据库。如果该环境变量可用,则使用它;如果不可用,则回退到使用本地的连接字符串来建立连接。

// db.rs
use std::env;

use mongodb::{Client, Collection};

use crate::models::booking_model::Booking;
use crate::models::dog_model::Dog;
use crate::models::owner_model::Owner;

pub struct Database {
booking: CollectionBooking,
dog: CollectionDog,
owner: CollectionOwner,
}

impl Database {
pub async fn init() -> Self {
let uri = match env::var("MONGO_URI") {
Ok(v) => v.to_string(),
Err(_) => "mongodb://localhost:27017/?directConnection=true".to_string(),
};

let client = Client::with_uri_str(uri).await.unwrap();
let db = client.database("dog_walking");

let booking: CollectionBooking = db.collection("booking");
let dog: CollectionDog = db.collection("dog");
let owner: CollectionOwner = db.collection("owner");

Database {
booking,
dog,
owner,
}
}
}

为了操作我们的数据库,我们将依赖于 mongodb crate 提供的功能。

创建函数

针对 ownerdog 和 booking 这三种不同的实体,我们需要分别实现三种不同的方法来进行数据库操作。

// db.rs

impl Database {
// other functions
pub async fn create_owner(&self, owner: Owner) -> ResultInsertOneResult, Error {
let result = self
.owner
.insert_one(owner, None)
.await
.ok()
.expect("Error creating owner");

Ok(result)
}

pub async fn create_dog(&self, dog: Dog) -> ResultInsertOneResult, Error {
let result = self
.dog
.insert_one(dog, None)
.await
.ok()
.expect("Error creating dog");

Ok(result)
}

pub async fn create_booking(&self, booking: Booking) -> ResultInsertOneResult, Error {
let result = self
.booking
.insert_one(booking, None)
.await
.ok()
.expect("Error creating booking");

Ok(result)
}
}

取消

我们的取消方法也很简单。我们将使用参数booking_id,并简单地将cancelled的值更新为false

// db.rs

impl Database {
// other functions

pub async fn cancel_booking(&self, booking_id: &str) -> ResultUpdateResult, Error {
let result = self
.booking
.update_one(
doc! {
"_id": ObjectId::from_str(booking_id).expect("Failed to parse booking_id")
},
doc! {
"$set": doc! {
"cancelled": true
}
},
None,
)
.await
.ok()
.expect("Error cancelling booking");

Ok(result)
}
}

获取完整预订

我们的最后一项功能是获取包含相关主人和狗信息的完整预订数据,这一操作相对复杂,因为我们需要利用 MongoDB 的聚合功能来执行相关的查找操作。幸运的是,所有这些复杂的处理都可以在单个数据库查询中完成。

需要注意的是,本文并非 MongoDB 的教程,因此我不会在此深入讲解聚合操作的细节。但简而言之,如果我们在筛选预订时考虑了时间因素(例如,只查看未来的预订)并且这些预订未被取消,那么我们就可以通过聚合查询来找到与特定预订相关联的主人(owner)和狗(dog)信息。

// db.rs

impl Database {
// other functions

pub async fn get_bookings(&self) -> ResultVec<FullBooking, Error> {
let now: SystemTime = Utc::now().into();

let mut results = self
.booking
.aggregate(
vec![
doc! {
"$match": {
"cancelled": false,
"start_time": {
"$gte": DateTime::from_system_time(now)
}
}
},
doc! {
"$lookup": doc! {
"from": "owner",
"localField": "owner",
"foreignField": "_id",
"as": "owner"
}
},
doc! {
"$unwind": doc! {
"path": "$owner"
}
},
doc! {
"$lookup": doc! {
"from": "dog",
"localField": "owner._id",
"foreignField": "owner",
"as": "dogs"
}
},
],
None,
)
.await
.ok()
.expect("Error getting bookings");

let mut bookings: VecFullBooking = Vec::new();

while let Some(result) = results.next().await {
match result {
Ok(doc) => {
let booking: FullBooking =
from_document(doc).expect("Error converting document to FullBooking");
bookings.push(booking);
}
Err(err) => panic!("Error getting booking: {}", err),
}
}

Ok(bookings)
}
}

聚合查询返回的结果类型是 Cursor<Document>。为了将这些文档转换为我们期望的返回类型 Vec<FullBooking>,我们需要借助 futures_util::stream::StreamExt 提供的 next 方法。

结合之前提到的所有数据库操作方法,我们的 db.rs 文件现在将包含以下内容的大致框架:

// db.rs
use std::{env, str::FromStr, time::SystemTime};

use chrono::Utc;
use futures_util::stream::StreamExt;
use mongodb::{
bson::{doc, extjson::de::Error, from_document, oid::ObjectId, DateTime},
results::{InsertOneResult, UpdateResult},
Client, Collection,
};

use crate::models::booking_model::{Booking, FullBooking};
use crate::models::dog_model::Dog;
use crate::models::owner_model::Owner;

pub struct Database {
booking: CollectionBooking,
dog: CollectionDog,
owner: CollectionOwner,
}

impl Database {
pub async fn init() -> Self {
let uri = match env::var("MONGO_URI") {
Ok(v) => v.to_string(),
Err(_) => "mongodb://localhost:27017/?directConnection=true".to_string(),
};

let client = Client::with_uri_str(uri).await.unwrap();
let db = client.database("dog_walking");

let booking: CollectionBooking = db.collection("booking");
let dog: CollectionDog = db.collection("dog");
let owner: CollectionOwner = db.collection("owner");

Database {
booking,
dog,
owner,
}
}

pub async fn create_owner(&self, owner: Owner) -> ResultInsertOneResult, Error {
let result = self
.owner
.insert_one(owner, None)
.await
.ok()
.expect("Error creating owner");

Ok(result)
}

pub async fn create_dog(&self, dog: Dog) -> ResultInsertOneResult, Error {
let result = self
.dog
.insert_one(dog, None)
.await
.ok()
.expect("Error creating dog");

Ok(result)
}

pub async fn create_booking(&self, booking: Booking) -> ResultInsertOneResult, Error {
let result = self
.booking
.insert_one(booking, None)
.await
.ok()
.expect("Error creating booking");

Ok(result)
}

pub async fn get_bookings(&self) -> ResultVec<FullBooking, Error> {
let now: SystemTime = Utc::now().into();

let mut results = self
.booking
.aggregate(
vec![
doc! {
"$match": {
"cancelled": false,
"start_time": {
"$gte": DateTime::from_system_time(now)
}
}
},
doc! {
"$lookup": doc! {
"from": "owner",
"localField": "owner",
"foreignField": "_id",
"as": "owner"
}
},
doc! {
"$unwind": doc! {
"path": "$owner"
}
},
doc! {
"$lookup": doc! {
"from": "dog",
"localField": "owner._id",
"foreignField": "owner",
"as": "dogs"
}
},
],
None,
)
.await
.ok()
.expect("Error getting bookings");

let mut bookings: VecFullBooking = Vec::new();

while let Some(result) = results.next().await {
match result {
Ok(doc) => {
let booking: FullBooking =
from_document(doc).expect("Error converting document to FullBooking");
bookings.push(booking);
}
Err(err) => panic!("Error getting booking: {}", err),
}
}

Ok(bookings)
}

pub async fn cancel_booking(&self, booking_id: &str) -> ResultUpdateResult, Error {
let result = self
.booking
.update_one(
doc! {
"_id": ObjectId::from_str(booking_id).expect("Failed to parse booking_id")
},
doc! {
"$set": doc! {
"cancelled": true
}
},
None,
)
.await
.ok()
.expect("Error cancelling booking");

Ok(result)
}
}

为了更好地组织我们的代码,我们需要一个小的 mod.rs 文件来公开我们的服务模块。

// mod.rs
pub mod db;

至此,我们已经完成了与数据库交互所需的大部分功能开发。接下来,就是定义HTTP路由的时刻了!

添加 HTTP 路由

现在,我们已经做好了编写终端节点的准备!

到目前这个阶段,其实我们已经完成了最艰巨的工作。对于每一个路由,我们只需在 Database 服务中调用相应的函数即可。利用Actix框架提供的宏,我们可以非常便捷地为每个路由指定所需的方法和路径。这样,当客户端发送请求时,就能够触发相应的业务逻辑,并与数据库进行交互,最终返回期望的结果。

Dog 路由实现

在定义Dog路由时,我们不仅要指定路由的处理方法和路径,还需要将客户端通过JSON请求发送的字段数据克隆到我们的Dog请求结构体中。

// dog_route.rs
use crate::{
models::dog_model::{Dog, DogRequest},
services::db::Database,
};
use actix_web::{
post,
web::{Data, Json},
HttpResponse,
};

#[post("/dog")]
pub async fn create_dog(db: DataDatabase, request: JsonDogRequest) -> HttpResponse {
match db
.create_dog(
Dog::try_from(DogRequest {
owner: request.owner.clone(),
name: request.name.clone(),
age: request.age.clone(),
breed: request.breed.clone(),
})
.expect("Error converting DogRequest to Dog."),
)
.await
{
Ok(booking) => HttpResponse::Ok().json(booking),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}

Owner  路由实现

对于 owner 路由,我们将遵循相同的模式。

// owner_route.rs
use crate::{
models::owner_model::{Owner, OwnerRequest},
services::db::Database,
};
use actix_web::{
post,
web::{Data, Json},
HttpResponse,
};

#[post("/owner")]
pub async fn create_owner(db: DataDatabase, request: JsonOwnerRequest) -> HttpResponse {
match db
.create_owner(
Owner::try_from(OwnerRequest {
name: request.name.clone(),
email: request.email.clone(),
phone: request.phone.clone(),
address: request.address.clone(),
})
.expect("Error converting OwnerRequest to Owner."),
)
.await
{
Ok(booking) => HttpResponse::Ok().json(booking),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}

Booking  路由实现

预订功能相较于其他功能会更为复杂,因为它涉及到三个不同的端点。然而,对于create端点来说,其实现模式与上述的Dog路由是相似的。

// booking_route.rs
use crate::{
models::booking_model::{Booking, BookingRequest},
services::db::Database,
};
use actix_web::{
get, post, put,
web::{Data, Json, Path},
HttpResponse,
};

#[post("/booking")]
pub async fn create_booking(db: DataDatabase, request: JsonBookingRequest) -> HttpResponse {
match db
.create_booking(
Booking::try_from(BookingRequest {
owner: request.owner.clone(),
start_time: request.start_time.clone(),
duration_in_minutes: request.duration_in_minutes.clone(),
})
.expect("Error converting BookingRequest to Booking."),
)
.await
{
Ok(booking) => HttpResponse::Ok().json(booking),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}

fetch端点甚至更简单,因为我们没有要解析的JSON主体。

// booking_route.rs

// existing code

#[get("/bookings")]
pub async fn get_bookings(db: DataDatabase) -> HttpResponse {
match db.get_bookings().await {
Ok(bookings) => HttpResponse::Ok().json(bookings),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}

最后,我们需要更新路线以取消预订。我们将使用add一个动态的id到路径中,在我们调用db.cancel_booking函数之前需要提取它。

// booking_route.rs

// existing code

#[put("/booking/{id}/cancel")]
pub async fn cancel_booking(db: DataDatabase, path: Path(String,)) -> HttpResponse {
let id = path.into_inner().0;

match db.cancel_booking(id.as_str()).await {
Ok(result) => HttpResponse::Ok().json(result),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}

总之,该文件应如下所示:

// booking_route.rs
use crate::{
models::booking_model::{Booking, BookingRequest},
services::db::Database,
};
use actix_web::{
get, post, put,
web::{Data, Json, Path},
HttpResponse,
};

#[get("/bookings")]
pub async fn get_bookings(db: DataDatabase) -> HttpResponse {
match db.get_bookings().await {
Ok(bookings) => HttpResponse::Ok().json(bookings),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}

#[put("/booking/{id}/cancel")]
pub async fn cancel_booking(db: DataDatabase, path: Path(String,)) -> HttpResponse {
let id = path.into_inner().0;

match db.cancel_booking(id.as_str()).await {
Ok(result) => HttpResponse::Ok().json(result),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}

#[post("/booking")]
pub async fn create_booking(db: DataDatabase, request: JsonBookingRequest) -> HttpResponse {
match db
.create_booking(
Booking::try_from(BookingRequest {
owner: request.owner.clone(),
start_time: request.start_time.clone(),
duration_in_minutes: request.duration_in_minutes.clone(),
})
.expect("Error converting BookingRequest to Booking."),
)
.await
{
Ok(booking) => HttpResponse::Ok().json(booking),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}

确保将我们的每个路由文件添加到mod.rs

// mod.rs
pub mod booking_route;
pub mod dog_route;
pub mod owner_route;

整合所有路由到HTTP服务器

在完成所有路由的定义和实现后,我们可以将它们整合到HTTP服务器上,并进行测试。回到main.rs文件,我们需要为每个路由添加一个service调用,以便将它们注册到Actix的HTTP服务器中。

// main.rs
mod models;
mod routes;
mod services;

use actix_web::{get, web::Data, App, HttpResponse, HttpServer, Responder};
use routes::{
booking_route::{cancel_booking, create_booking, get_bookings},
dog_route::create_dog,
owner_route::create_owner,
};
use services::db::Database;

#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello Medium!")
}

#[actix_web::main]
async fn main() -> std::io::Result() {
let db = Database::init().await;
let db_data = Data::new(db);
HttpServer::new(move || {
App::new()
.app_data(db_data.clone())
.service(hello)
.service(create_owner)
.service(create_dog)
.service(create_booking)
.service(get_bookings)
.service(cancel_booking)
})
.bind(("127.0.0.1", 5001))?
.run()
.await
}

我们已成功达成预设目标!但如何验证其实际效果呢?幸运的是,我们可以借助Postman或cURL等工具来轻松测试各个端点。以下是一些cURL命令示例,帮助你快速上手测试:

## POST /owner
curl --location '[http://localhost:5001/owner](http://localhost:5001/owner)' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Joe Bloggs",
"email": "joe.bloggs@example.org",
"phone": "+44800001066",
"address": "123 Main St"
}'

## POST /dog
curl --location '[http://localhost:5001/dog](http://localhost:5001/dog)' \
--header 'Content-Type: application/json' \
--data '{
"owner": "66080390d0e4f489a8e0bbd0",
"name": "Chuffey",
"age": 7,
"breed": "Miniature Schnauzer"
}'

## POST /booking
curl --location '[http://localhost:5001/booking](http://localhost:5001/booking)' \
--header 'Content-Type: application/json' \
--data '{
"owner": "66080390d0e4f489a8e0bbd0",
"start_time": "2024-04-30T10:00:00.000Z",
"duration_in_minutes": 30
}'

## GET /bookings
curl --location '[http://localhost:5001/bookings](http://localhost:5001/bookings)'

## PUT /booking/{id}/cancel
curl --location --request PUT '[http://localhost:5001/booking/66080390d0e4f489a8e0bbd0/cancel](http://localhost:5001/booking/66080390d0e4f489a8e0bbd0/cancel)'

项目圆满结束啦!

希望本文能为你带来实质性的帮助。在Rust中构建API服务器的方法灵活多样,这只是其中一种解决方案!

原文链接:https://www.bretcameron.com/blog/how-to-build-an-api-server-with-rust

#你可能也喜欢这些API文章!