使用 Rust 的 Rocket 框架入门

更新于 2026-01-18

Joshua Mo 2023-12-13

尽管 Rust 常被认为是一门令初学者望而生畏的语言,但在提升 Rust 可用性方面已取得诸多进展——尤其是在 Rust 后端 Web 框架领域。随着 Rocket v0.5 的发布,现在正是尝试这一标志性 Rust Web 框架的最佳时机。此前由于组织问题,Rocket 一度陷入停滞,如今它已在 Rocket Web Foundation 的管理下全速前进。

本文将重点介绍如何使用 Rocket 0.5 入门,同时也会包含从 0.4 升级到 0.5 的主要注意事项(如需升级)。


迁移到 Rocket 0.5

你需要了解的主要变化包括:

  • rocket_contrib 已被弃用 —— 现在你需要直接在 Rocket 中启用所需功能(并根据需要使用 rocket_dyn_templatesrocket_sync_db_poolsrocket_db_pools)。
  • Rocket 现在主要基于 async/await,并重新导出(re-export)了 tokio,因此如果你需要 Tokio 相关功能,可直接通过 rocket::tokio 使用。
  • 现在实现 trait 时必须使用 #[rocket::async_trait]
  • 查询参数(Query parameters)现在使用 FromForm!
  • Server-Sent Events(SSE)WebSockets 现已获得官方支持。

以上只是升级要点的简要概述。从 0.4 到 0.5 的改动极为广泛。如需完整迁移指南,请参阅官方文档


快速入门

你可以通过以下命令创建一个新的 Web 服务项目:

cargo init example-rocket-api
cd example-rocket-api
cargo add rocket

Rocket 0.5 内部使用 Tokio(如发布日志所述)。不过,你不需要显式地将 tokio 添加到你的项目中即可编写 Rocket API。Rocket 已重新导出 Tokio,因此你可以通过 rocket::tokio 访问 Tokio 功能。当然,如果你需要 Rocket 未暴露的特定 Tokio 功能,则仍需单独添加 tokio crate。


路由(Routing)

请求(Requests)

在 Rocket 中,路由需通过宏定义。与许多其他框架类似,Rocket 广泛使用宏来简化开发(否则可能需要像 Axum 那样使用复杂的 trait bounds)。例如:

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

你可以通过 rocket::build().mount() 将路由挂载到应用中:

let rocket = rocket::build().mount("/hello", routes![index]);

这意味着当你启动服务并在浏览器中访问 /hello 路径时,会显示 “Hello, world!”。注意:实际路径是挂载点与路由路径的组合。你也可以在 routes! 宏中添加多个路由,将它们统一挂载到同一个前缀下。


处理 JSON 数据

与其他 Rust Web 框架一样,Rocket 使用 serde 来处理 JSON 的序列化与反序列化。首先添加带 derive 特性的 serde

cargo add serde -F derive

然后在函数参数中使用:

use rocket::serde::json::Json;
use serde::Deserialize;

#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct Task<'r> {
    description: &'r str,
    complete: bool
}

#[post("/todo", data = "<task>")]
fn new(task: Json<Task<'_>>) { /* .. */ }

更多关于在 Rocket 中使用 JSON 的信息,请参阅官方文档


表单(Forms)

Rocket 在表单处理方面非常强大。你可以通过 FromForm 派生宏快速上手:

use rocket::form::Form;

#[derive(FromForm)]
struct Task<'r> {
    complete: bool,
    r#type: &'r str, // 使用 raw identifier 避免关键字冲突
}

#[post("/todo", data = "<task>")]
fn new(task: Form<Task<'_>>) { /* .. */ }

默认情况下,缺失字段、重复字段或额外字段都会被允许:缺失字段使用默认值填充,重复或额外字段则被忽略。

若要禁止这种行为,可使用 Strict 类型:

use rocket::form::Strict;

#[derive(FromForm)]
struct Task<'r> {
    complete: Strict<bool>,
    r#type: &'r str,
}

或者在 handler 函数中直接包裹:

#[post("/todo", data = "<task>")]
fn new(task: Form<Strict<Task<'_>>>) { /* .. */ }

Rocket 在表单处理上远超其他框架,尤其是对 multipart 表单的支持:你无需额外配置,处理方式与普通表单一致。相比之下,在 Axum 中你需要启用 multipart 特性,并手动遍历每个字段,代码往往冗长且不优雅。

你还可以嵌套表单结构:

#[derive(FromForm)]
struct MyForm<'r> {
    owner: Person<'r>,
    pet: Pet<'r>,
}

#[derive(FromForm)]
struct Person<'r> {
    name: &'r str
}

#[derive(FromForm)]
struct Pet<'r> {
    name: &'r str,
    #[field(validate = eq(true))]
    good_pet: bool,
}

这种方式非常适合将复杂表单拆分为多个子结构,并在不同场景下灵活组合。

你也可以通过路径参数捕获动态值:

#[get("/hello/<name>")]
fn hello(name: &str) -> String {
    format!("Hello, {}!", name)
}

Rocket 请求守卫(Request Guards)

请求守卫是 Rocket 最强大的特性之一。它们是代表特定验证策略的类型,可作为 handler 函数的参数传入,从而将验证逻辑拆分为多个独立函数,避免单一函数承担过多职责。

例如:

use rocket::response::Redirect;

#[get("/login")]
fn login() -> Template { /* .. */ }

#[get("/admin")]
fn admin_panel(admin: AdminUser) -> &'static str {
    "Hello, administrator. This is the admin panel!"
}

#[get("/admin", rank = 2)]
fn admin_panel_user(user: User) -> &'static str {
    "Sorry, you must be an administrator to access this page."
}

#[get("/admin", rank = 3)]
fn admin_panel_redirect() -> Redirect {
    Redirect::to(uri!(login))
}

这里,AdminUser 是一个请求守卫。如果无法满足(例如用户不是管理员),Rocket 会尝试匹配下一个 rank = 2 的路由;若仍失败,则重定向到登录页。

要实现自定义请求守卫,你的类型必须实现 FromRequest trait:

use rocket::request::{self, Request, FromRequest};

pub struct MyError;
pub struct MyType;

#[rocket::async_trait]
impl<'a> FromRequest<'a> for MyType {
    type Error = MyError;

    async fn from_request(req: &'a Request<'_>) -> request::Outcome<Self, Self::Error> {
        Outcome::Success(MyType)
    }
}

提示:如果你需要在特定路由上应用认证等逻辑,强烈建议使用请求守卫而非中间件(fairings),因为 fairings 是全局生效的。


错误处理(Error Handling)

Rocket 的错误处理机制不同于其他框架。它不依赖 trait 实现,而是使用称为 “catcher” 的错误处理器(类似于 Axum 中的 fallback 服务)。

你可以这样定义一个默认 catcher:

#[catch(default)]
fn default_catcher(status: Status, request: &Request) -> String {
    format!("ERROR: {} - {}", status.code, status.reason)
}

#[launch]
fn rocket() -> _ {
    rocket::build().register("/", catchers![default_catcher])
}

也可以针对特定状态码(如 404)定义 catcher:

#[catch(404)]
fn foo_not_found() -> &'static str {
    "Foo 404"
}

⚠️ 注意:必须通过 .register() 将 catcher 注册到 Rocket 应用中,否则不会生效!


添加数据库

通常在 Rust 中设置数据库需要手动管理连接。在 Rocket 中,你可以使用 rocket-db-pools crate(以 PostgreSQL 为例):

cargo add rocket-db-pools -F sqlx_postgres

然后初始化数据库连接:

use rocket_db_pools::{sqlx, Database};

#[derive(Database)]
#[database("sqlx")]
struct DB(sqlx::PgPool);

#[launch]
fn rocket() -> _ {
    rocket::build().attach(DB::init())
}

你需要:

  1. 本地运行 PostgreSQL(可通过 Docker 等方式)
  2. Rocket.toml 中配置连接信息(后文详述)

之后即可在 handler 中作为请求守卫使用:

#[get("/<id>")]
async fn read(mut db: Connection<DB>, id: i64) -> Option<String> {
   sqlx::query("SELECT content FROM logs WHERE id = ?").bind(id)
       .fetch_one(&mut **db).await
       .and_then(|r| Ok(r.try_get(0)?))
       .ok()
}

使用 Shuttle 部署

Shuttle 上,你可以通过注解自动获取连接池:

#[shuttle_runtime::main]
async fn rocket(
    #[shuttle_shared_db::Postgres] pool: PgPool,
) -> shuttle_rocket::ShuttleRocket {
    let state = AppState { pool };

    pool.execute(include_str!("../schema.sql"))
        .await
        .map_err(CustomError::new)?;

    let rocket = rocket::build()
        .mount("/todo", routes![retrieve, add])
        .manage(state);

    Ok(rocket.into())
}
  • 本地通过 Docker 提供数据库
  • 部署时由 Shuttle 自动完成
  • 也支持 AWS RDS(无需 AWS 知识)

应用状态(App State)

Rocket 允许你通过 状态结构体(state struct) 在路由间共享数据(如数据库连接池)。

首先定义状态并注册:

use sqlx::PgPool;

struct AppState {
    db: PgPool,
}

async fn main() {
    let db = connect_to_db();

    rocket::build()
        .manage(AppState { db });
}

与 Axum 不同,Rocket 的状态结构体不需要实现 Clone,只需满足 Send + Sync

在 handler 中使用状态(通过引用):

use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct Thing {
   message: String,
}

#[get("/count/<id>")]
fn get_data(state: &State<AppState>, id: i32) -> Vec<Thing> {
    let result = sqlx::query_as::<_, Thing>("SELECT * FROM TABLE WHERE id = 1")
        .bind(id)
        .fetch_all(&state.db)
        .await
        .unwrap();

    result
}

🔒 编译时检查:未通过 .manage() 注册的状态无法在 handler 中使用,避免运行时错误。

在请求守卫中访问状态

由于 State 本身是请求守卫,在自定义守卫中需通过 request.guard::<&State<AppState>>() 获取:

use rocket::State;
use rocket::request::{self, Request, FromRequest};
use rocket::outcome::IntoOutcome;
use rocket::http::Status;

struct Item<'r>(&'r str);

struct AppState {
    db: PgPool,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for Item<'r> {
    type Error = ();

    async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, ()> {
        // 方法一:使用 guard
        let outcome = request.guard::<&State<AppState>>().await
            .map(|my_config| Item(&my_config.user_val));

        // 方法二:使用 Rocket::state()
        let outcome = request.rocket().state::<AppState>()
            .map(|my_config| Item(&my_config.user_val))
            .or_forward(Status::InternalServerError);

        outcome
    }
}

Fairings(中间件)

Rocket 中的中间件称为 Fairings,用于在请求处理前后执行逻辑(如日志、认证、请求改写等)。

⚠️ 重要:Fairings 是全局生效的。若只需对部分路由应用逻辑(如认证),应使用 请求守卫 而非 Fairing。

实现 Fairing 需定义结构体并实现 Fairing trait(需满足 Send + Sync + 'static):

use std::io::Cursor;
use std::sync::atomic::{AtomicUsize, Ordering};

use rocket::{Request, Data, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{Method, ContentType, Status};

struct Counter {
    get: AtomicUsize,
    post: AtomicUsize,
}

#[rocket::async_trait]
impl Fairing for Counter {
    fn info(&self) -> Info {
        Info {
            name: "GET/POST Counter",
            kind: Kind::Request | Kind::Response
        }
    }

    async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
        match request.method() {
            Method::Get => self.get.fetch_add(1, Ordering::Relaxed),
            Method::Post => self.post.fetch_add(1, Ordering::Relaxed),
            _ => return
        };
    }

    async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
        if response.status() != Status::NotFound {
            return
        }

        if request.method() == Method::Get && request.uri().path() == "/counts" {
            let get_count = self.get.load(Ordering::Relaxed);
            let post_count = self.post.load(Ordering::Relaxed);
            let body = format!("Get: {}\nPost: {}", get_count, post_count);

            response.set_status(Status::Ok);
            response.set_header(ContentType::Plain);
            response.set_sized_body(body.len(), Cursor::new(body));
        }
    }
}

使用 AdHoc 简化 Fairing

为避免大量样板代码,可使用 AdHoc 从闭包创建 Fairing:

rocket::ignite()
    .attach(AdHoc::on_launch("Launch Printer", |_| {
        println!("Rocket is about to launch! Exciting! Here we go...");
    }))

静态文件(Static Files)

Rocket 提供三种静态文件服务方式:

1. 单文件服务(NamedFile

use std::path::{Path, PathBuf};
use rocket::fs::NamedFile;

#[get("/<file..>")]
async fn files(file: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::new("static/").join(file)).await.ok()
}

2. 文件夹服务(FileServer

rocket.mount("/public", FileServer::from("static/"))

3. HTML 模板(rocket_dyn_templates

首先添加依赖:

cargo add rocket-dyn-templates

然后使用模板:

use rocket_dyn_templates::Template;

#[get("/")]
fn index() -> Template {
    Template::render("index", context! {
        foo: 123,
    })
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![/* .. */])
        .attach(Template::fairing())
}
  • 支持 Tera.tera 文件)和 Handlebars.hbs 文件)
  • 模板文件默认放在 templates/ 目录
  • 无需手动构建模板列表,Rocket 自动处理

配置(Configuration)

Rocket 使用基于 figment crate 的配置系统,支持通过 Rocket.toml 文件管理不同环境(开发/测试/生产)的配置。

示例 Rocket.toml

# Rocket.toml
## 所有环境的默认配置
[default]
address = "0.0.0.0"
limits = { form = "64 kB", json = "1 MiB" }

[default.tls]
key = "path/to/key.pem"     # DER 编码的私钥路径
certs = "path/to/certs.pem" # DER 编码的证书链路径

## debug 模式(cargo build)
[debug]
port = 8000
limits = { json = "10MiB" }  # 仅覆盖 json 限制

## 自定义 nyc 环境
[nyc]
port = 9001

## release 模式(cargo build --release)
[release]
port = 9999
ip_header = false
secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk="

可配置项包括:地址、端口、keep-alive 超时、请求体大小限制、TLS 证书、密钥等。


部署(Deployment)

传统 Rust 后端部署通常依赖 Docker(可配合 cargo-chef 优化构建)。但如果你使用 Shuttle,只需运行:

shuttle deploy

无需任何配置即可完成部署。


结语

感谢阅读!尽管 Rocket 曾因发展停滞而在追求前沿框架的开发者中失宠,但 0.5 版本带来了大量革新。希望本文能帮助你快速构建一个功能完备的 Rust Web API!