实用 Rust Web 开发 - API Rest

2025-06-07

实用 Rust Web 开发 - API Rest

更新:根据这个问题,异步不适用于 Diesel,因此,to_async来自的方法web::get可能无法按预期工作,它会工作但不是你想要的方式,所以,老实说,你可能会将它更改为to

这是一系列博客文章中的第一篇,展示了如何使用 Rust 进行 Web 开发,我尝试尽可能实用,使用已经为这项工作选择的工具。

我将从基础开始,直到我们可以创建基本的 API Rest 端点,用于列出、创建、编辑和删除虚构网上商店的产品。

我会一步一步地讲解,但是最好先阅读一下Rust 书籍,了解一些有关 Rust 语言的知识。

我们需要做的第一件事是安装 Rust,我们可以访问https://www.rust-lang.org/tools/install并按照说明进行操作,因为我使用的是 Linux,所以示例代码取自该操作系统,但是您也可以在 Windows 或 Mac 上尝试。

在终端中执行下一步并按照说明进行操作:curl https://sh.rustup.rs -sSf | sh

您可以通过运行来验证 Rust 是否正确安装rustc -V,它将显示已安装的 rustc 版本。

接下来我们要做的是创建一个新项目,我们可以将其命名为 mystore,在终端窗口中运行以下命令:cargo new mystore --bin

如果一切正确,我们将能够看到一个名为 mystore 的文件夹,我们可以看到 Rust 项目的基本结构:

树我的商店

接下来我们需要一个 Web 框架,我们将使用 actix-web,一个基于 Actix(一个 Actor 框架)的高级框架。在 中添加以下代码行cargo.toml

[dependencies]
actix = "0.8"
actix-web = "1.0.0-beta"
Enter fullscreen mode Exit fullscreen mode

现在,当您执行时cargo build,将安装板条箱并编译项目。

我们将从一个 hello world 示例开始,添加下一行代码src/main.rs

extern crate actix_web;
use actix_web::{HttpServer, App, web, HttpRequest, HttpResponse};

// Here is the handler, 
// we are returning a json response with an ok status 
// that contains the text Hello World
fn index(_req: HttpRequest) -> HttpResponse  {
    HttpResponse::Ok().json("Hello world!")
}

fn main() {
    // We are creating an Application instance and 
    // register the request handler with a route and a resource 
    // that creates a specific path, then the application instance 
    // can be used with HttpServer to listen for incoming connections.
    HttpServer::new(|| App::new().service(
             web::resource("/").route(web::get().to_async(index))))
        .bind("127.0.0.1:8088")
        .unwrap()
        .run();
}

Enter fullscreen mode Exit fullscreen mode

cargo run在终端中执行,然后转到http://localhost:8088/并查看结果,如果您可以在浏览器中看到文本 Hello world!,则一切按预期工作。

现在,我们将选择数据库驱动程序,在本例中为diesel,我们在中添加一个依赖项Cargo.toml

[dependencies]
diesel = { version = "1.0.0", features = ["postgres"] }
dotenv = "0.9.0"
Enter fullscreen mode Exit fullscreen mode

如果我们执行,cargo build那么板条箱就会被安装,项目也会被编译。

安装 cli 工具也是个好主意cargo install diesel_cli

如果您在安装 diesel_cli 时遇到问题,可能是因为缺少数据库驱动程序,因此请确保包含它们,如果您使用 Ubuntu,则可能需要安装postgresql-server-dev-all

在 bash 中执行下一个命令:

$ echo DATABASE_URL=postgres://postgres:@localhost/mystore > .env
Enter fullscreen mode Exit fullscreen mode

现在,diesel 设置数据库的一切准备就绪,运行diesel setup即可创建数据库。如果您在配置 postgres 时遇到问题,请查看本指南

让我们创建一个表来处理产品:

diesel migration generate create_products
Enter fullscreen mode Exit fullscreen mode

Diesel CLI 将创建两个迁移文件,up.sqldown.sql

up.sql

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name VARCHAR NOT NULL,
  stock FLOAT NOT NULL,
  price INTEGER --representing cents
)
Enter fullscreen mode Exit fullscreen mode

down.sql

DROP TABLE products
Enter fullscreen mode Exit fullscreen mode

应用迁移:

diesel migration run
Enter fullscreen mode Exit fullscreen mode

我们将加载我们需要的库main.rs

src/main.rs

#[macro_use]
extern crate diesel;
extern crate dotenv;
Enter fullscreen mode Exit fullscreen mode

接下来我们创建一个文件来处理数据库连接,我们将其命名为 db_connection.rb 并将其保存在 src 中。

src/db_connection.rs

use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;

pub fn establish_connection() -> PgConnection {
    dotenv().ok(); // This will load our .env file.

    // Load the DATABASE_URL env variable into database_url, in case of error
    // it will through a message "DATABASE_URL must be set"
    let database_url = env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");

    // Load the configuration in a postgres connection, 
    // the ampersand(&) means we're taking a reference for the variable. 
    // The function you need to call will tell you if you have to pass a
    // reference or a value, borrow it or not.
    PgConnection::establish(&database_url)
        .expect(&format!("Error connecting to {}", database_url))
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们将创建第一个资源:产品列表。

我们首先需要的是几个结构,一个用于创建资源,另一个用于获取资源,在本例中是用于产品。

我们可以将它们保存在名为 models 的文件夹中,但在此之前,我们需要一种方法来加载我们的文件,我们添加下几行main.rs

src/main.rs

pub mod schema;
pub mod models;
pub mod db_connection;
Enter fullscreen mode Exit fullscreen mode

我们需要在模型文件夹中创建一个名为mod.rs

src/models/mod.rs

pub mod product;
Enter fullscreen mode Exit fullscreen mode

src/models/product.rs

use crate::schema::products;

#[derive(Queryable)]
pub struct Product {
    pub id: i32,
    pub name: String,
    pub stock: f64,
    pub price: Option<i32> // For a value that can be null, 
                           // in Rust is an Option type that 
                           // will be None when the db value is null
}

#[derive(Insertable)]
#[table_name="products"]
pub struct NewProduct {
    pub name: Option<String>,
    pub stock: Option<f64>,
    pub price: Option<i32>
}
Enter fullscreen mode Exit fullscreen mode

因此,让我们添加一些代码来获取产品列表,我们将创建一个名为 ProductList 的新结构来处理产品列表,并添加一个函数列表来从数据库中获取产品,将下一个块添加到models/product.rs

// This will tell the compiler that the struct will be serialized and 
// deserialized, we need to install serde to make it work.
#[derive(Serialize, Deserialize)] 
pub struct ProductList(pub Vec<Product>);

impl ProductList {
    pub fn list() -> Self {
        // These four statements can be placed in the top, or here, your call.
        use diesel::RunQueryDsl;
        use diesel::QueryDsl;
        use crate::schema::products::dsl::*;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        let result = 
            products
                .limit(10)
                .load::<Product>(&connection)
                .expect("Error loading products");

        // We return a value by leaving it without a comma
        ProductList(result)
    }
}
Enter fullscreen mode Exit fullscreen mode

我这样做是为了让我们可以自由地向该结构添加任何特征,我们不能对 Vector 这样做,因为我们不拥有它,ProductList 使用 Rust 中的新类型模式。

现在,我们只需要一个句柄来回答产品列表的请求,我们将使用serde它将数据序列化为 json 响应。

我们需要编辑Cargo.tomlmain.rsmodels/product.rs

Cargo.toml

serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
Enter fullscreen mode Exit fullscreen mode

main.rs

pub mod handlers; // This goes to the top to load the next handlers module 

extern crate serde;
extern crate serde_json;
#[macro_use] 
extern crate serde_derive;
Enter fullscreen mode Exit fullscreen mode

src/models/product.rs

#[derive(Queryable, Serialize, Deserialize)]
pub struct Product {
    pub id: i32,
    pub name: String,
    pub stock: f64,
    pub price: Option<i32>
}
Enter fullscreen mode Exit fullscreen mode

mod.rs添加名为的文件src/handlers

pub mod products;
Enter fullscreen mode Exit fullscreen mode

我们可以在处理程序文件夹中创建一个名为的文件products.rs

src/handlers/products.rs

use actix_web::{HttpRequest, HttpResponse };

use crate::models::product::ProductList;

// This is calling the list method on ProductList and 
// serializing it to a json response
pub fn index(_req: HttpRequest) -> HttpResponse {
    HttpResponse::Ok().json(ProductList::list())
}
Enter fullscreen mode Exit fullscreen mode

我们需要将索引处理程序添加到我们的服务器中以main.rs获得 Rest API 的第一部分,更新文件使其看起来像这样:

src/main.rs

pub mod schema;
pub mod db_connection;
pub mod models;
pub mod handlers;

#[macro_use]
extern crate diesel;
extern crate dotenv;
extern crate serde;
extern crate serde_json;
#[macro_use] 
extern crate serde_derive;

extern crate actix;
extern crate actix_web;
extern crate futures;
use actix_web::{App, HttpServer, web};

fn main() {
    let sys = actix::System::new("mystore");

    HttpServer::new(
    || App::new()
        .service(
            web::resource("/products")
                .route(web::get().to_async(handlers::products::index))
        ))
    .bind("127.0.0.1:8088").unwrap()
    .start();

    println!("Started http server: 127.0.0.1:8088");
    let _ = sys.run();
}
Enter fullscreen mode Exit fullscreen mode

让我们在终端运行中添加一些数据并查看它是什么样子:

psql -U postgres -d mystore -c "INSERT INTO products(name, stock, price) VALUES ('shoes', 10.0, 100); INSERT INTO products(name, stock, price) VALUES ('hats', 5.0, 50);"
Enter fullscreen mode Exit fullscreen mode

然后执行:

cargo run
Enter fullscreen mode Exit fullscreen mode

最后转到http://localhost:8088/products

如果一切按预期进行,您应该会在 json 值中看到几个产品。

创建产品

向结构添加Deserialize特征NewProduct并添加函数来创建产品:

#[derive(Insertable, Deserialize)]
#[table_name="products"]
pub struct NewProduct {
    pub name: String,
    pub stock: f64,
    pub price: Option<i32>
}

impl NewProduct {

    // Take a look at the method definition, I'm borrowing self, 
    // just for fun remove the & after writing the handler and 
    // take a look at the error, to make it work we would need to use into_inner (https://actix.rs/api/actix-web/stable/actix_web/struct.Json.html#method.into_inner)
    // which points to the inner value of the Json request.
    pub fn create(&self) -> Result<Product, diesel::result::Error> {
        use diesel::RunQueryDsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();
        diesel::insert_into(products::table)
            .values(self)
            .get_result(&connection)
    }
}
Enter fullscreen mode Exit fullscreen mode

然后添加一个处理程序来创建产品:

use crate::models::product::NewProduct;
use actix_web::web;

pub fn create(new_product: web::Json<NewProduct>) -> Result<HttpResponse, HttpResponse> {

    // we call the method create from NewProduct and map an ok status response when
    // everything works, but map the error from diesel error 
    // to an internal server error when something fails.
    new_product
        .create()
        .map(|product| HttpResponse::Ok().json(product))
        .map_err(|e| {
            HttpResponse::InternalServerError().json(e.to_string())
        })
}
Enter fullscreen mode Exit fullscreen mode

最后添加相应的路由并启动服务器:

src/main.rs

    HttpServer::new(
    || App::new()
        .service(
            web::resource("/products")
                .route(web::get().to_async(handlers::products::index))
                .route(web::post().to_async(handlers::products::create))
        ))
    .bind("127.0.0.1:8088").unwrap()
    .start();
Enter fullscreen mode Exit fullscreen mode
cargo run
Enter fullscreen mode Exit fullscreen mode

我们可以创建一个新产品:

curl http://127.0.0.1:8088/products \                                                                                                
        -H "Content-Type: application/json" \
        -d '{"name": "socks", "stock": 7, "price": 2}'

Enter fullscreen mode Exit fullscreen mode

显示产品

src/models/product.rs

impl Product {
    pub fn find(id: &i32) -> Result<Product, diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        products::table.find(id).first(&connection)
    }
}
Enter fullscreen mode Exit fullscreen mode

src/handlers/products.rs


use crate::models::product::Product;

pub fn show(id: web::Path<i32>) -> Result<HttpResponse, HttpResponse> {
    Product::find(&id)
        .map(|product| HttpResponse::Ok().json(product))
        .map_err(|e| {
            HttpResponse::InternalServerError().json(e.to_string())
        })
}
Enter fullscreen mode Exit fullscreen mode

src/main.rs

    HttpServer::new(
    || App::new()
        .service(
            web::resource("/products")
                .route(web::get().to_async(handlers::products::index))
                .route(web::post().to_async(handlers::products::create))
        )
        .service(
            web::resource("/products/{id}")
                .route(web::get().to_async(handlers::products::show))
        )
    )
    .bind("127.0.0.1:8088").unwrap()
    .start();
Enter fullscreen mode Exit fullscreen mode
cargo run
Enter fullscreen mode Exit fullscreen mode

如果一切正常,你应该在http://127.0.0.1:8088/products/1中看到一只鞋子

删除产品

向产品模型添加新方法:

src/models/product.rs

impl Product {
    pub fn find(id: &i32) -> Result<Product, diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        products::table.find(id).first(&connection)
    }

    pub fn destroy(id: &i32) -> Result<(), diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::schema::products::dsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        // Take a look at the question mark at the end, 
        // it's a syntax sugar that allows you to match 
        // the return type to the one in the method signature return, 
        // as long as it is the same error type, it works for Result and Option.
        diesel::delete(dsl::products.find(id)).execute(&connection)?;
        Ok(())
    }
}
Enter fullscreen mode Exit fullscreen mode

src/handlers/products.rs


pub fn destroy(id: web::Path<i32>) -> Result<HttpResponse, HttpResponse> {
    Product::destroy(&id)
        .map(|_| HttpResponse::Ok().json(()))
        .map_err(|e| {
            HttpResponse::InternalServerError().json(e.to_string())
        })
}
Enter fullscreen mode Exit fullscreen mode

src/main.rs

    HttpServer::new(
    || App::new()
        .service(
            web::resource("/products")
                .route(web::get().to_async(handlers::products::index))
                .route(web::post().to_async(handlers::products::create))
        )
        .service(
            web::resource("/products/{id}")
                .route(web::get().to_async(handlers::products::show))
                .route(web::delete().to_async(handlers::products::destroy))
        )
    )
    .bind("127.0.0.1:8088").unwrap()
    .start();
Enter fullscreen mode Exit fullscreen mode
cargo run
Enter fullscreen mode Exit fullscreen mode

让我们删除一只鞋子:

curl -X DELETE http://127.0.0.1:8088/products/1 \
        -H "Content-Type: application/json"

Enter fullscreen mode Exit fullscreen mode

你不应该在http://127.0.0.1:8088/products中看到鞋子

更新产品

将 AsChangeset 特征添加到 NewProduct,这样您就可以将结构直接传递给更新,否则您需要指定要更新的每个字段。

src/models/product.rs

#[derive(Insertable, Deserialize, AsChangeset)]
#[table_name="products"]
pub struct NewProduct {
    pub name: Option<String>,
    pub stock: Option<f64>,
    pub price: Option<i32>
}

impl Product {
    pub fn find(id: &i32) -> Result<Product, diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        products::table.find(id).first(&connection)
    }

    pub fn destroy(id: &i32) -> Result<(), diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::schema::products::dsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        diesel::delete(dsl::products.find(id)).execute(&connection)?;
        Ok(())
    }

    pub fn update(id: &i32, new_product: &NewProduct) -> Result<(), diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::schema::products::dsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        diesel::update(dsl::products.find(id))
            .set(new_product)
            .execute(&connection)?;
        Ok(())
    }
}
Enter fullscreen mode Exit fullscreen mode

src/handlers/product.rs

pub fn update(id: web::Path<i32>, new_product: web::Json<NewProduct>) -> Result<HttpResponse, HttpResponse> {
    Product::update(&id, &new_product)
        .map(|_| HttpResponse::Ok().json(()))
        .map_err(|e| {
            HttpResponse::InternalServerError().json(e.to_string())
        })
}
Enter fullscreen mode Exit fullscreen mode

src/main.rs

    HttpServer::new(
    || App::new()
        .service(
            web::resource("/products")
                .route(web::get().to_async(handlers::products::index))
                .route(web::post().to_async(handlers::products::create))
        )
        .service(
            web::resource("/products/{id}")
                .route(web::get().to_async(handlers::products::show))
                .route(web::delete().to_async(handlers::products::destroy))
                .route(web::patch().to_async(handlers::products::update))
        )
    )
    .bind("127.0.0.1:8088").unwrap()
    .start();
Enter fullscreen mode Exit fullscreen mode
cargo run
Enter fullscreen mode Exit fullscreen mode

现在,让我们为产品添加库存:

curl -X PATCH http://127.0.0.1:8088/products/3 \
        -H "Content-Type: application/json" \
        -d '{"stock": 8}'
Enter fullscreen mode Exit fullscreen mode

您现在应该有 8 双袜子:http://127.0.0.1: 8088/products/3 。

在此处查看完整的源代码

Rust 不是最简单的编程语言,但它的优点克服了问题,Rust 允许您长期编写高性能和高效的应用程序。

文章来源:https://dev.to/werner/practical-rust-web-development-api-rest-29g1
PREV
我是 Wes Bos,问我任何事情!
NEXT
SQL:外连接