LoginSignup
16
8

More than 3 years have passed since last update.

Rust で REST/gRPC/GraphQL API サーバー達を立てる

Last updated at Posted at 2020-12-19

この記事はRust 3 Advent Calendar 2020の 20 日目の記事です。
前日の記事はhibi221b様の『top 500 Rustlang repositories』です。分野問わず様々なリポジトリがリストアップされております。

本記事のまとめ

Rust で下記 API を用いるサーバーの実装を試しました。本記事ではそれらのサンプルコードおよび参考にした記事を共有します。

  • gRPC(RPC 系)
  • REST
  • GraphQL

こちらのリポジトリに、本記事に関連するコードが含まれております。

本編の目次

  1. 今回対象とする API の参考記事
  2. 実装例
    1. gRPC
    2. REST
    3. GraphQL
  3. 終わりに

今回対象とする API の参考記事

Web API の一般的な説明については、例えば下記の記事が参考になります。

Web 関係で使用される API は多数存在します。altexsoft 社の記事によれば、API のアーキテクチャーのスタイルは下のように分類されるようです。

今回実装する gRPC, REST, GraphQL については下の Qiita 記事に詳しくありますので、そちらをご参照ください。

実装例

gRPC

gRPC のライブラリは grpc-ecosystem/awesome-grpc リポジトリに掲載されています。
上記リポジトリを 2020/12/19 に確認した限りでは、Rust で gRPC の機能を提供しているクレートは下記のようです。

  • grpc-rs - The gRPC library for Rust built on C Core library and futures
  • grpc-rust - Rust implementation of gRPC
  • tower-grpc - A client and server gRPC implementation based on Tower
  • tonic - A native gRPC client & server implementation with async/await support

今回はこれらの中で tonic を使用します。tonic リポジトリのサンプルこちらの Qiita 記事を参考にしました。

ファイル構成など

ファイル構成は次のようになります。

grpc_hello_server/
    proto/
        hello_server.proto
    src/
        client.rs
        server.rs
    build.rs
    Cargo.toml

Cargo.toml の中身を以下に示します。依存クレートは tonicprosttokio です。

Cargo.toml
[package]
name = "grpc-hello-server"
version = "0.1.0"
authors = ["XXXX"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[[bin]]
name = "grpc-hello-server"
path = "src/server.rs"

[[bin]]
name = "grpc-hello-client"
path = "src/client.rs"

[dependencies]
tonic = "0.3.1"
prost = "^0.6"
tokio = { version = "^0.2.13", features = ["macros"] }

[build-dependencies]
tonic-build = "0.3"

gRPC では API の仕様を proto ファイルに定義します。
今回実装した hello_server.proto の中身を以下に示します。

hello_server.proto
syntax = "proto3";
package hello_server;

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

定義された proto ファイルをコンパイルし、それを他のファイルで呼び出せるようにします。
コンパイルするための関数を build.rs の中に定義しています。

build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/hello_server.proto")?;
    Ok(())
}

上記でコンパイルされると、下記 client.rsserver.rs のように proto で定義されたものを利用できるようになります。
※コンパイル自体は client.rs などを作成した後で問題ありません。

client.rsの中身を以下に示します。

client.rs
// hello_server.proto 内のアイテムをモジュールとしてインポート
pub mod hello_server {
    tonic::include_proto!("hello_server");
}

// 上記モジュール内のアイテムを呼び出す
use hello_server::greeter_client::GreeterClient;
use hello_server::HelloRequest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = GreeterClient::connect("http://[::1]:50051").await?;

    let request = tonic::Request::new(HelloRequest {
        name: "Tonic".into(),
    });

    let response = client.say_hello(request).await?;

    println!("RESPONSE={:?}", response);

    Ok(())
}

server.rs の中身を以下に示します。

server.rs
use tonic::{transport::Server, Request, Response, Status};

// hello_server.proto 内のアイテムをモジュールとしてインポート
pub mod hello_server {
    tonic::include_proto!("hello_server"); 
}

// 上記モジュール内のアイテムを呼び出す
use hello_server::greeter_server::{Greeter, GreeterServer};
use hello_server::{HelloReply, HelloRequest};

#[derive(Debug, Default)]
pub struct MyGreeter {}

#[tonic::async_trait]
impl Greeter for MyGreeter {
    // Return an instance of type HelloReply
    async fn say_hello(
        &self,
        // Accept request of type HelloRequest
        request: Request<HelloRequest>, 
    ) -> Result<Response<HelloReply>, Status> { 
        println!("Got a request: {:?}", request);

        let reply = HelloReply {
            // We must use .into_inner() as the fields of gRPC requests and responses are private
            message: format!("Hello {}!", request.into_inner().name).into(), 
        };

        // Send back our formatted greeting
        Ok(Response::new(reply)) 
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse().unwrap();
    let greeter = MyGreeter::default();

    println!("GreeterServer listening on {}", addr);

    Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr)
        .await?;

    Ok(())
}

テスト方法

  1. grpc_hello_server/ 内で cargo run --bin grpc-hello-server を実行。
  2. 別のコマンドプロンプトで cargo run --bin grpc-hello-client を実行。

REST

REST API は Actix Web 単体でも実装可能です。公式サイトの記事を参考にしました。

ファイル構成など

ファイル構成は次のようになります。

rest_hello_server/
    src/
        client.rs
        server.rs
    Cargo.toml

Cargo.toml の中身を以下に示します。依存クレートは actix-webactix-httpawc です。

Cargo.toml
[package]
name = "rest-hello-server"
version = "0.1.0"
authors = ["XXXX"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "3.3.2"
actix-http = "^2.2.0"
awc = "^2.0.3"

[[bin]]
name = "rest-hello-server"
path = "src/server.rs"

[[bin]]
name = "rest-hello-client"
path = "src/client.rs"

client.rs の中身を以下に示します。

client.rs
use actix_http::Error;

#[actix_web::main]
async fn main() -> Result<(), Error> {
    let client = awc::Client::new();
    // Create request builder, configure request and send
    let addr: &str = "http://localhost:8080/";
    let mut response = client
        .get(addr)
        .send()
        .await?;

    // server http response
    println!("Response: {:?}", response);

    // read response body
    let body = response.body().await?;
    println!("Downloaded: {:?} bytes, {:?}", body.len(), body);

    // Get a response from the second endpoint
    let addr: &str = "http://localhost:8080/hey";
    let mut response = client
        .get(addr)
        .send()
        .await?;

    // server http response
    println!("Response: {:?}", response);

    // read response body
    let body = response.body().await?;
    println!("Downloaded: {:?} bytes, {:?}", body.len(), body);

    Ok(())
}

server.rs の中身を以下に示します。

server.rs
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};

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

#[post("/echo")]
async fn echo(req_body: String) -> impl Responder {
    HttpResponse::Ok().body(req_body)
}

async fn manual_hello() -> impl Responder {
    HttpResponse::Ok().body("Hey there!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
            .service(echo)
            .route("/hey", web::get().to(manual_hello))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

テスト方法

  1. rest_hello_server/ 内で cargo run --bin rest-hello-server を実行。
  2. 別のコマンドプロンプトで cargo run --bin rest-hello-client を実行。

GraphQL

GraphQL の公式サイトに、以下の GraphQL in Rust ライブラリが掲載されています。今回は juniper を利用しました。

実装にあたって、actix/examples/juniperを参考にしました。

ファイル構成など

ファイル構成は次のようになります。

graphql_server/
    src/
        scheme.rs
        server.rs
    Cargo.toml

Cargo.toml の中身を以下に示します。

Cargo.toml
[package]
name = "graphql-server"
version = "0.1.0"
authors = ["XXXX"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "3"
actix-cors = "0.4.0"
serde = "1.0.103"
serde_json = "1.0.44"
serde_derive = "1.0.103"
juniper = "0.14.2"

[[bin]]
name = "graphql-server"
path = "src/server.rs"

GraphQL ではやり取りするデータのスキームを定義します。今回は scheme.rs 内に以下のようにスキームを定義しました。

schema.rs
use juniper::FieldResult;
use juniper::RootNode;

use juniper::{GraphQLInputObject, GraphQLObject};

// オブジェクトの定義
#[derive(GraphQLObject)]
#[graphql(description = "Hello struct")]
struct Hello {
    id: String,
    message: String,
}

#[derive(GraphQLInputObject)]
#[graphql(description = "NewHello struct")]
struct NewHello {
    message: String,
}

// クエリの定義(Get用)
pub struct QueryRoot;

#[juniper::object]
impl QueryRoot {
    fn human(id: String) -> FieldResult<Hello> {
        Ok(Hello {
            id: "0".to_owned(),
            message: "Hello GraphQL!".to_owned(),
        })
    }
}

// ミューテーションの定義(Post用)
pub struct MutationRoot;

#[juniper::object]
impl MutationRoot {
    fn create_hello(new_hello: NewHello) -> FieldResult<Hello> {
        Ok(Hello {
            id: "1234".to_owned(),
            message: new_hello.message,
        })
    }
}

pub type Schema = RootNode<'static, QueryRoot, MutationRoot>;

pub fn create_schema() -> Schema {
    Schema::new(QueryRoot {}, MutationRoot {})
}

server.rs の中身を以下に示します。こちらはほぼ actix/examples/juniper 内サンプルコードのままです。

server.rs

use std::io;
use std::sync::Arc;

use actix_cors::Cors;
use actix_web::{web, App, Error, HttpResponse, HttpServer};
use juniper::http::graphiql::graphiql_source;
use juniper::http::GraphQLRequest;

mod schema;

use crate::schema::{create_schema, Schema};

async fn graphiql() -> HttpResponse {
    let html = graphiql_source("http://127.0.0.1:8080/graphql");
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(html)
}

async fn graphql(
    st: web::Data<Arc<Schema>>,
    data: web::Json<GraphQLRequest>,
) -> Result<HttpResponse, Error> {
    let user = web::block(move || {
        let res = data.execute(&st, &());
        Ok::<_, serde_json::error::Error>(serde_json::to_string(&res)?)
    })
    .await?;
    Ok(HttpResponse::Ok()
        .content_type("application/json")
        .body(user))
}

#[actix_web::main]
async fn main() -> io::Result<()> {
    // Create Juniper schema
    let schema = std::sync::Arc::new(create_schema());

    // Start http server
    HttpServer::new(move || {
        App::new()
            .data(schema.clone())
            .wrap(
                Cors::new()
                    .allowed_methods(vec!["POST", "GET"])
                    .supports_credentials()
                    .max_age(3600)
                    .finish(),
            )
            .service(web::resource("/graphql").route(web::post().to(graphql)))
            .service(web::resource("/graphiql").route(web::get().to(graphiql)))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

クライアントサーバーについて、GraphQL には client サーバーとして QraphiQL が実装されていますので、それを利用します。

テスト方法

  1. graphql_server/ 内で cargo run --bin graphql-server を実行。
  2. http://127.0.0.1:8080/graphiql にアクセスして GET/POST を行う。

終わりに

本記事では API サーバーを Rust で立てる例を紹介しました。ほかにもいろいろな機能(並列処理など)が実装されているので、それらを有効に活用すると、サーバー実装がより楽しく、効率的になると思われます。

16
8
1

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
16
8