LoginSignup
162

More than 3 years have passed since last update.

Rust RocketでのWebAPIサーバーの書き方を解説してみる

Last updated at Posted at 2018-07-09

Rocketは素晴らしいWebアプリケーションフレームワークで、サンプルコードも充実しているけど、WebAPIサーバーを作る包括的なチュートリアルが無かったように思えたので書いてみることにする。なお、このリポジトリコードはこの記事を参考にしている。

この解説に使ったコードは以下。

Table of Contents

Setup

# rustupをインストールする.
$ curl https://sh.rustup.rs -sSf | /bin/bash -s -- -y --default-toolchain nightly

Run

$ cd rocket-webapi
$ cargo run


🔧  Configured for development.
    => address: localhost
    => port: 8000
    => log: normal
    => workers: 8
    => secret key: generated
    => limits: forms = 32KiB
    => tls: disabled
🛰  Mounting '/':
    => GET /
    => GET /todos
    => GET /todos/<todoid>
🚀  Rocket has launched from http://localhost:8000

Usage

  • Hello, world!

    http://localhost:8000
    
  • Get all ToDOs

    http://localhost:8000/todos
    
  • Get ToDO by ID

    http://localhost:8000/todos/10
    

Tutorial

なぜRustなのか?

Rustは型安全でゼロコスト抽象化を実現したシステムプログラミング言語だ。巷ではC言語の代替と言われることもあるが、実際使ってみるとより安全なC++としての趣きが強いと思う。筆者は10年以上C++でMMOゲームや金融系のハイパフォーマンスサーバーを書いてきて、C++17とかModern CMakeとか、Rangeとかテンプレートプログラミングとかを色々追ってきたが、Rustに出会ってからC++の最新を追うのをやめた。Rustは例えるなら(まだない)C++23に安全性とSaneなメタプロシステムと最高のビルドシステムと最高のパッケージマネージャが付いてきた、みたいな感じ。それほどまでにRustは素晴らしい機能と言語としての表現力を持っている(と思う)。

Rocketとは?

RocketはRustで書かれたタイプセーフなマイクロウェブアプリケーションフレームワークで、PythonのFlaskに似ている感じ。それでは、シンプルなTODOアプリのWebAPIサーバーを作ってみて、Rocketの使い方を解説してみる。

Hello Rocket!

まずは、Hello, worldを返すだけのアプリを作ってみる。

  • プロジェクト作成

    $ cargo new rocket-webapi
    $ cd rocket-webapi
    
  • rust-toolchain

    nightly
    
  • Cargo.toml

    [package]
    name = "rocket-jsonapi"
    version = "0.1.0"
    
    [dependencies]
    rocket = "0.4"
    rocket_contrib = { version = "0.4", features = ["json"] }
    
  • src/main.rs

    #![feature(proc_macro_hygiene)]
    #![feature(decl_macro)]
    
    #[macro_use]
    extern crate rocket;
    
    /// GETがきたときに"Hello, world!"というレスポンスを返す
    #[get("/")]
    fn index() -> &'static str {
        "Hello, world!"
    }
    
    fn main() {
        rocket::ignite()
            .mount("/", routes![index])  // ここにルーティングをセットする
            .launch();
    }
    

実行してみる。

$ cargo run


🔧  Configured for development.
    => address: localhost
    => port: 8000
    => log: normal
    => workers: 8
    => secret key: generated
    => limits: forms = 32KiB
    => tls: disabled
🛰  Mounting '/':
    => GET /
🚀  Rocket has launched from http://localhost:8000
$ curl http://localhost:8000
Hello, world!

動いた! \(^o^)/

ここでコンパイルエラーが出た人は、以下を試してみてほしい

# Rust toolchainを更新する
rustup update

ToDOアプリのWebAPIをつくる

次にいきおいでToDoのWebAPIを作ってみる。

  • Cargo.toml

    [package]
    name = "rocket-webapi"
    version = "0.1.0"
    
    [dependencies]
    rocket = "0.4"
    rocket_contrib = { version = "0.4", features = ["json"] }
    # serdeのcrateを追加する
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0.0"
    
  • main.rs

    #![feature(proc_macro_hygiene)]
    #![feature(decl_macro)]
    
    #[macro_use]
    extern crate rocket;
    
    mod models;
    mod routes;
    
    // WebAPIのURLルーティングはroutes.rsに移動する
    use routes::*;
    
    fn main() {
        rocket::ignite()
            .mount("/", routes![index, todos, new_todo, todo_by_id])
            .launch();
    }
    
  • routes.rs

    // JSONを返すのに必要
    use rocket_contrib::json::Json;
    
    use crate::models::ToDo;
    
    #[get("/")]
    pub fn index() -> &'static str {
        "Hello, world!"
    }
    
    /// TODOリストを返す。
    /// Jsonの型がResponderをimplしているので、JSON文字列を返すことができる
    #[get("/todos")]
    pub fn todos() -> Json<Vec<ToDo>> {
        Json(vec![ToDo {
            id: 1,
            title: "Read Rocket tutorial".into(),
            description: "Read https://rocket.rs/guide/quickstart/".into(),
            done: false,
        }])
    }
    
    /// 新しいTODOを作成する
    /// POSTの時はこうする
    #[post("/todos", data = "<todo>")]
    pub fn new_todo(todo: Json<ToDo>) -> String {
        format!("Accepted post request! {:?}", todo.0)
    }
    
    /// TODOを取得する
    #[get("/todos/<todoid>")]
    pub fn todo_by_id(todoid: u32) -> String {
        let todo = ToDo {
            id: 1,
            title: "Read Rocket tutorial".into(),
            description: "Read https://rocket.rs/guide/quickstart/".into(),
            done: false,
        };
        format!("{:?}", todo)
    }
    
  • models.rs

    use serde::{Deserialize, Serialize};
    
    /// TODOのモデルはmodels.rsに定義
    #[derive(Debug, Serialize, Deserialize)]
    pub struct ToDo {
        pub id: u32,
        pub title: String,
        pub description: String,
        pub done: bool,
    }
    

実行してみる。

$ cargo run

🔧  Configured for development.
    => address: localhost
    => port: 8000
    => log: normal
    => workers: 8
    => secret key: generated
    => limits: forms = 32KiB
    => tls: disabled
🛰  Mounting '/':
    => GET /
    => GET /todos
    => POST /todos
    => GET /todos/<todoid>
🚀  Rocket has launched from http://localhost:8000

curlでリクエストしてみる。

$ curl -i http://localhost:8000/todos

HTTP/1.1 200 OK
Content-Type: application/json
Server: Rocket
Content-Length: 111
Date: Wed, 04 Jul 2018 13:44:50 GMT

[{"id":1,"title":"Read Rocket tutorial","description":"Read https://rocket.rs/guide/quickstart/","done":false}]

OK, いい感じ。

次、POST。

$ curl -i -H "Content-Type: application/json" -X POST -d '{"id": 100, "title":"Read this book", "description": "http://shop.oreilly.com/product/0636920040385.do", "done": false}' http://localhost:20000/todos

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Server: Rocket
Content-Length: 142
Date: Thu, 05 Jul 2018 03:55:22 GMT

Accepted post request! ToDo { id: 100, title: "Read this book", description: "http://shop.oreilly.com/product/0636920040385.do", done: false }

POSTもOK。

Responder

上記の例でJson型の戻り値を返すとJSONの文字列がレスポンスとして返った。この仕組みを実現いているのがResponderトレイトだ。Rocketのルーティングの関数はすべてResponderトレイトをimplしなければならない。

難しそうい聞こえるが、実際にはRocketがいろいろな型のResponderトレイトをあらかじめimplしといてくれるので、自分でimplする場面は意外に少ないかもしれない。以下に主なResponderのimplを示す。

レスポンス
&'static str, &str, String text/plainの文字列が返る
NamedFile ファイルの文字列が返る
Redirect 別のURLにリダイレクトする
Stream HTTPストリーミングレスポンスが返る
Json application/jsonのJSON文字列が返る
Template Templateをレンダリングした結果が返る
rocket::response::statusにある型 例えばAcceptedの場合あ203 Acceptedになる
Option Some(T)の場合はTのResponder、Noneの場合は404 Not Foundになる
Result Ok(T)の場合はT、Err(E)の場合はUのResponderの結果が返る

最後の3つはWrapping Responderと言われておりWrapした中身のResponderの結果を装飾したり、中身の型によって動きを動的に返る役割を持つ。

以下の例を見てみよう。

  • JSONを一つ返す

    fn sample() -> Json<ToDo> {
        Json(ToDo {
            id: 1,
            title: "Read Rocket tutorial".into(),
            description: "Read https://rocket.rs/guide/quickstart/".into(),
            done: false,
        })
    }
    
    $ curl http://localhost:8000/todos
    
    {"id":1,"title":"Read Rocket tutorial","description":"Read https://rocket.rs/guide/quickstart/","done":false}
    

なるほど。

  • Vectorにしてみる

    fn sample() -> Vec<Json<ToDo>> {
        Json(vec![ToDo {
            id: 1,
            title: "Read Rocket tutorial".into(),
            description: "Read https://rocket.rs/guide/quickstart/".into(),
            done: false,
        }])
    }
    
    $ curl http://localhost:8000/todos
    
    [{"id":1,"title":"Read Rocket tutorial","description":"Read https://rocket.rs/guide/quickstart/","done":false}]
    

JSONがArrayになった。おぉいいね。

  • rocket::response::status::Acceptedでwrapすると

    use rocket::response::status::Accepted;
    fn sample() -> Accepted<Vec<Json<ToDo>>> {
        Accepted(Some(Json(vec![ToDo {
            id: 1,
            title: "Read Rocket tutorial".into(),
            description: "Read https://rocket.rs/guide/quickstart/".into(),
            done: false,
        }])))
    }
    
    $ curl -i http://localhost:8000/todos
    HTTP/1.1 202 Accepted
    Content-Type: application/json
    Server: Rocket
    Content-Length: 111
    Date: Fri, 06 Jul 2018 15:32:19 GMT
    
    [{"id":1,"title":"Read Rocket tutorial","description":"Read https://rocket.rs/guide/quickstart/","done":false}]
    

    Status codeが202 Acceptedになる。

スバラシイ。

長くなったのでここまで。次回以降は以下のいずれかを解説したい。

  • パラメータのパース、バリデーション
  • Configuration
  • エラー処理
  • データベース
  • ロギング

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
162