Rocket

開発者体験を重視したRust Webフレームワーク。型安全性とコード生成機能により、素早い開発が可能。安定版に移行済み。

RustフレームワークWeb開発非同期タイプセーフ

GitHub概要

rwf2/Rocket

A web framework for Rust.

ホームページ:https://rocket.rs
スター25,323
ウォッチ271
フォーク1,608
作成日:2016年3月18日
言語:Rust
ライセンス:Other

トピックス

frameworkrocketrustwebweb-developmentweb-framework

スター履歴

rwf2/Rocket Star History
データ取得日時: 2025/8/13 01:43

フレームワーク

Rocket

概要

Rocketは、Rust言語で高性能かつ安全なWebアプリケーションを構築するための非同期フレームワークです。

詳細

Rocket(ロケット)は、Rustエコシステムにおいて最も人気の高いWebフレームワークの一つで、2017年にSergio Benitezによって開発が開始されました。使いやすさ、セキュリティ、拡張性、高速性を重視した設計で、Rustの型システムを最大限に活用してコンパイル時の正確性を保証します。プロシージャラルマクロを積極的に活用することで、ボイラープレートコードを最小限にし、直感的なAPIを提供しています。非同期コアを採用したasync/awaitをサポートし、高いコンカレンシー性能を実現します。Request Guards、Responders、Fairings(ミドルウェア)などの獨自の概念により、リクエストのライフサイクルを精密に制御できます。シールド機能によるセキュリティヘッダーの自動設定、型安全なURIジェネレーション、JSON・HTTP/2サポートなど、モダンWeb開発に必要な機能を結合し、高品質なアプリケーションの高速開発を実現しています。

メリット・デメリット

メリット

  • 型安全性: Rustの型システムによるコンパイル時の正確性保証
  • 高パフォーマンス: メモリ安全性と高速実行の両立
  • 直感的API: プロシージャラルマクロによる簡潔なコード
  • 非同期サポート: async/awaitによるコンカレンシー処理
  • ビルトインセキュリティ: Shield機能による自動セキュリティ対策
  • 柔軟性: Fairingsとカスタムガードによる拡張性
  • HTTP/2サポート: デフォルトでHTTP/2をサポート
  • 優秀なテスト機能: 統合テスト機能

デメリット

  • Rustの学習曲線: Rust言語自体の習得が必要
  • コンパイル時間: 大規模プロジェクトでのコンパイル時間の長さ
  • ライフタイムの制約: Rustの所有権システムによる制約
  • エコシステムの規模: Node.jsやPythonと比較して小さいライブラリエコシステム
  • Nightly Rust依存: 一部機能では不安定版のRustが必要(改善中)
  • デバッグの難しさ: 非同期処理のデバッグの複雑さ

主要リンク

書き方の例

Hello World

#[macro_use] extern crate rocket;

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

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

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, hello])
}

ルーティングとパラメータ

use rocket::serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct User {
    id: u64,
    name: String,
    email: String,
}

// パスパラメータ
#[get("/users/<id>")]
fn get_user(id: u64) -> String {
    format!("User ID: {}", id)
}

// クエリパラメータ
#[get("/search?<q>&<limit>")]
fn search(q: String, limit: Option<usize>) -> String {
    let limit = limit.unwrap_or(10);
    format!("Search: {} (limit: {})", q, limit)
}

// マルチパスセグメント
#[get("/files/<path..>")]
fn files(path: std::path::PathBuf) -> String {
    format!("File path: {}", path.display())
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![get_user, search, files])
}

JSON APIとRequest Guards

use rocket::serde::{Deserialize, Serialize, json::Json};
use rocket::{State, get, post, put, delete};
use std::collections::HashMap;
use std::sync::Mutex;

#[derive(Serialize, Deserialize, Clone)]
#[serde(crate = "rocket::serde")]
struct User {
    id: Option<u64>,
    name: String,
    email: String,
}

type UserStore = Mutex<HashMap<u64, User>>;
type IdCounter = Mutex<u64>;

#[get("/users")]
fn get_users(store: &State<UserStore>) -> Json<Vec<User>> {
    let store = store.lock().unwrap();
    let users: Vec<User> = store.values().cloned().collect();
    Json(users)
}

#[get("/users/<id>")]
fn get_user(id: u64, store: &State<UserStore>) -> Option<Json<User>> {
    let store = store.lock().unwrap();
    store.get(&id).map(|user| Json(user.clone()))
}

#[post("/users", data = "<user>")]
fn create_user(
    user: Json<User>,
    store: &State<UserStore>,
    counter: &State<IdCounter>
) -> Json<User> {
    let mut store = store.lock().unwrap();
    let mut counter = counter.lock().unwrap();
    
    *counter += 1;
    let id = *counter;
    let mut new_user = user.into_inner();
    new_user.id = Some(id);
    
    store.insert(id, new_user.clone());
    Json(new_user)
}

#[put("/users/<id>", data = "<user>")]
fn update_user(id: u64, user: Json<User>, store: &State<UserStore>) -> Option<Json<User>> {
    let mut store = store.lock().unwrap();
    if store.contains_key(&id) {
        let mut updated_user = user.into_inner();
        updated_user.id = Some(id);
        store.insert(id, updated_user.clone());
        Some(Json(updated_user))
    } else {
        None
    }
}

#[delete("/users/<id>")]
fn delete_user(id: u64, store: &State<UserStore>) -> Option<()> {
    let mut store = store.lock().unwrap();
    store.remove(&id).map(|_| ())
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(UserStore::new(HashMap::new()))
        .manage(IdCounter::new(0))
        .mount("/api", routes![get_users, get_user, create_user, update_user, delete_user])
}

フォーム処理とバリデーション

use rocket::form::{Form, Errors};
use rocket::response::Redirect;
use rocket::serde::{Deserialize, Serialize};
use rocket::{get, post};

#[derive(FromForm, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct ContactForm {
    #[field(validate = len(1..50))]
    name: String,
    #[field(validate = contains('@'))]
    email: String,
    #[field(validate = len(10..500))]
    message: String,
}

#[get("/contact")]
fn contact_form() -> &'static str {
    r#"
    <form method="post" action="/contact">
        <input type="text" name="name" placeholder="Name" required>
        <input type="email" name="email" placeholder="Email" required>
        <textarea name="message" placeholder="Message" required></textarea>
        <button type="submit">Send</button>
    </form>
    "#
}

#[post("/contact", data = "<form>")]
fn submit_contact(form: Form<ContactForm>) -> Result<Redirect, String> {
    // フォームデータの処理
    println!("Received contact from {}: {}", form.name, form.message);
    Ok(Redirect::to("/thank-you"))
}

#[post("/contact", data = "<form>", rank = 2)]
fn contact_error(form: Errors<ContactForm>) -> String {
    format!("Form errors: {:?}", form)
}

#[get("/thank-you")]
fn thank_you() -> &'static str {
    "Thank you for your message!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![
        contact_form, submit_contact, contact_error, thank_you
    ])
}

非同期処理とデータベース

use rocket::serde::{Deserialize, Serialize, json::Json};
use rocket::{get, post, State};
use sqlx::{PgPool, Row};

#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct Post {
    id: Option<i32>,
    title: String,
    content: String,
    published: bool,
}

#[get("/posts")]
async fn get_posts(db: &State<PgPool>) -> Result<Json<Vec<Post>>, String> {
    let posts = sqlx::query!("SELECT id, title, content, published FROM posts")
        .fetch_all(db.inner())
        .await
        .map_err(|e| format!("Database error: {}", e))?
        .into_iter()
        .map(|row| Post {
            id: Some(row.id),
            title: row.title,
            content: row.content,
            published: row.published,
        })
        .collect();
    
    Ok(Json(posts))
}

#[get("/posts/<id>")]
async fn get_post(id: i32, db: &State<PgPool>) -> Result<Json<Post>, String> {
    let post = sqlx::query!("SELECT id, title, content, published FROM posts WHERE id = $1", id)
        .fetch_one(db.inner())
        .await
        .map_err(|e| format!("Database error: {}", e))?;
    
    Ok(Json(Post {
        id: Some(post.id),
        title: post.title,
        content: post.content,
        published: post.published,
    }))
}

#[post("/posts", data = "<post>")]
async fn create_post(post: Json<Post>, db: &State<PgPool>) -> Result<Json<Post>, String> {
    let record = sqlx::query!(
        "INSERT INTO posts (title, content, published) VALUES ($1, $2, $3) RETURNING id",
        post.title,
        post.content,
        post.published
    )
    .fetch_one(db.inner())
    .await
    .map_err(|e| format!("Database error: {}", e))?;
    
    let mut created_post = post.into_inner();
    created_post.id = Some(record.id);
    
    Ok(Json(created_post))
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(/* PgPool の初期化 */)
        .mount("/api", routes![get_posts, get_post, create_post])
}

ミドルウェア(Fairings)と認証

use rocket::fairing::{Fairing, Info, Kind};
use rocket::{Request, Response, Data};
use rocket::request::{self, Request as Req, FromRequest};
use rocket::outcome::Outcome;
use rocket::http::Status;

// ログフェアリング
pub struct RequestLogger;

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

    async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) {
        println!("Request: {} {}", req.method(), req.uri());
    }

    async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
        println!("Response: {} - {}", req.uri(), res.status());
    }
}

// 認証ガード
struct ApiKey(String);

#[rocket::async_trait]
impl<'r> FromRequest<'r> for ApiKey {
    type Error = &'static str;

    async fn from_request(req: &'r Req<'_>) -> request::Outcome<Self, Self::Error> {
        if let Some(key) = req.headers().get_one("X-API-Key") {
            if key == "secret-api-key" {
                Outcome::Success(ApiKey(key.to_string()))
            } else {
                Outcome::Error((Status::Unauthorized, "Invalid API key"))
            }
        } else {
            Outcome::Error((Status::BadRequest, "Missing API key"))
        }
    }
}

#[get("/protected")]
fn protected_route(_key: ApiKey) -> &'static str {
    "This is a protected route!"
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(RequestLogger)
        .mount("/", routes![protected_route])
}

テスト

#[cfg(test)]
mod tests {
    use super::*;
    use rocket::local::blocking::Client;
    use rocket::http::{Status, ContentType};

    #[test]
    fn test_hello_world() {
        let client = Client::tracked(rocket()).expect("valid rocket instance");
        let response = client.get("/").dispatch();
        assert_eq!(response.status(), Status::Ok);
        assert_eq!(response.into_string(), Some("Hello, world!".into()));
    }

    #[test]
    fn test_hello_name() {
        let client = Client::tracked(rocket()).expect("valid rocket instance");
        let response = client.get("/hello/John").dispatch();
        assert_eq!(response.status(), Status::Ok);
        assert_eq!(response.into_string(), Some("Hello, John!".into()));
    }

    #[test]
    fn test_json_api() {
        let client = Client::tracked(rocket()).expect("valid rocket instance");
        
        // POST request
        let new_user = User {
            id: None,
            name: "Test User".to_string(),
            email: "[email protected]".to_string(),
        };
        
        let response = client
            .post("/api/users")
            .header(ContentType::JSON)
            .json(&new_user)
            .dispatch();
        
        assert_eq!(response.status(), Status::Ok);
        
        // GET request
        let response = client.get("/api/users").dispatch();
        assert_eq!(response.status(), Status::Ok);
    }

    #[test]
    fn test_protected_route() {
        let client = Client::tracked(rocket()).expect("valid rocket instance");
        
        // Without API key
        let response = client.get("/protected").dispatch();
        assert_eq!(response.status(), Status::BadRequest);
        
        // With valid API key
        let response = client
            .get("/protected")
            .header(("X-API-Key", "secret-api-key"))
            .dispatch();
        assert_eq!(response.status(), Status::Ok);
    }
}