Guide / Reinhardt 0.3.15

Reinhardt 0.3.15のRESTサンプルでCRUD APIを試す

この記事の内容

Rustで小さなWeb APIを試したい方に向けて、ReinhardtのRESTサンプルを動かす手順を紹介します。題材は、タイトル・コード・言語を登録できるスニペット(短いコード片)の管理APIです。

サンプルを起動してCRUDと不正入力を試し、HTTPリクエストの受信からデータベースへの書き込みまで、処理の流れをコードで追います。対象は Reinhardt 0.3.15。このバージョンで必要なローカル設定ファイルと、管理コマンドのサーバー機能を有効にする指定も含めて説明します。

ローカル環境を用意する

Git、Cargoを含む Rust 1.96.0以上、Docker Engine、Python 3.11以上、Bash、curlが必要です。Docker Engineはあらかじめ起動しておいてください。Pythonはインフラ用のスクリプトで使います。付属スクリプトを直接実行するため、cargo-makeは必須ではありません。環境変数 CIREINHARDT_ENV で別の設定プロファイルが選ばれていない、通常のローカルシェルで実行します。

ポート 5432・6379・8000 を空けておきます。付属スクリプトは、examples-tutorial-rest-postgresexamples-tutorial-rest-redis という名前のコンテナを作成します。同名のコンテナがすでにある場合は作り直すため、保存されていたデータは消えます。データを残す必要がない、ローカルの検証環境で試してください。

タグを指定し、新しいディレクトリにリポジトリをクローンします。

git clone --depth 1 --branch '[email protected]' https://github.com/kent8192/reinhardt-web.git reinhardt-rest-demo
cd reinhardt-rest-demo/examples/examples-tutorial-rest
git rev-parse HEAD

表示されたコミットが c64a11e2dd945eab35ce3d56d812b0b78ff1b327 と一致することを確認します。このターミナルでは、以後のコマンドも examples/examples-tutorial-rest ディレクトリで実行します。

ローカル設定を補ってAPIを起動する

このタグには settings/local.toml も、そのひな形も含まれていません。インフラ用スクリプトが参照するローカル設定を作成します。

cat > settings/local.toml <<'TOML'
[core]
debug = true
allowed_hosts = ["localhost", "127.0.0.1"]
TOML

これはローカルでサンプルを試すための開発設定です。データベースの接続設定は、既存のベース設定から引き継ぎます。付属スクリプトでコンテナを起動します。

bash scripts/infra_up.sh

このスクリプトでPostgreSQLとRedisが起動します。スニペットAPIの保存先はPostgreSQLで、現在 API自体はRedisを使っていません。Redisはサンプル共通のインフラに含まれるため、一緒に起動します。

マイグレーションを適用します。

cargo run --features reinhardt/commands-server --bin manage -- migrate

続けて、開発サーバーを起動します。

cargo run --features reinhardt/commands-server --bin manage -- runserver 127.0.0.1:8000 --noreload

0.3.15では、reinhardt/commands-server というfeatureを明示的に有効にする必要があります。初回の検証では、デフォルトのfeature構成でもビルドとマイグレーションは成功しましたが、runserverServer feature not enabled を表示しました。HTTPリクエストを受け付けないまま、終了コード0で終了しています。上のコマンドは、管理コマンドで使うサーバー機能を有効にします。実際に起動できたかどうかは、次のHTTPリクエストで確認します。

初回のCargo実行では、サンプルと依存クレートのビルドが行われます。サーバーは起動したままにして、リクエストを送るために 別のターミナル を開いてください。

スニペットを作成して取得する

まず、登録されているスニペットの一覧を取得します。

curl -i http://127.0.0.1:8000/api/snippets/

データベースを作り直した直後なら、一覧は空のはずです。-i を付けると、HTTPステータスとレスポンスヘッダーも表示されます。次に、スニペットを一つ作成します。

curl -i -X POST http://127.0.0.1:8000/api/snippets/ \
  -H 'Content-Type: application/json' \
  -d '{"title":"Hello Reinhardt","code":"fn main() {}","language":"rust"}'

HTTPステータスが 201 になり、応答の snippet.id に作成したスニペットのIDが入っていることを確認します。次のコマンドを実行する前に、REPLACE_WITH_RETURNED_ID実際に返った整数のIDに置き換えてください。IDが1とは限らないので、応答に含まれる値を使います。以降の操作も、この2つ目のターミナルで行います。

SNIPPET_ID=REPLACE_WITH_RETURNED_ID

このIDを使ってスニペットを取得します。

curl -i "http://127.0.0.1:8000/api/snippets/${SNIPPET_ID}/"

HTTPステータスが 200 で、送信したタイトル・コード・言語が返ることを確認します。

更新、削除、不正入力を試す

同じIDを指定して内容を更新します。

curl -i -X PUT "http://127.0.0.1:8000/api/snippets/${SNIPPET_ID}/" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Updated snippet","code":"fn main() { println!(\"updated\"); }","language":"rust"}'

ステータスが 200 で、応答に Updated snippet と更新したコードが含まれることを確認します。続けて、このスニペットを削除します。

curl -i -X DELETE "http://127.0.0.1:8000/api/snippets/${SNIPPET_ID}/"

削除が成功すると、本文のない 204 の応答が返ります。同じIDでもう一度取得を試します。

curl -i "http://127.0.0.1:8000/api/snippets/${SNIPPET_ID}/"

検証時には、削除後の取得は 404 となり、本文に {"error":"Snippet not found"} が返りました。

次に、code の値を空文字列にしたJSONを送信します。

curl -i -X POST http://127.0.0.1:8000/api/snippets/ \
  -H 'Content-Type: application/json' \
  -d '{"title":"Invalid snippet","code":"","language":"rust"}'

JSONとしては正しいデータを送り、code の入力制約が働くか確かめます。検証時のステータスは 400 でした。エラー本文を確認したら、不正なレコードが追加されていないか、一覧も確認します。

curl -i http://127.0.0.1:8000/api/snippets/

ここまでの操作だけなら、最後の一覧は空のはずです。

処理の流れをコードで追う

リクエストの入口となるsrc/config/urls.rs::routesでは、/api/ の下にsnippetsのルーターを組み込んでいます。url_patternsには、一覧・作成・取得・更新・削除を担当するハンドラー関数が登録されています。

views.rs::createでは、リクエスト本文を Json<SnippetSerializer> で、データベース接続を #[inject] による依存注入で受け取ります。pre_validate = true の指定により、ハンドラー本体を実行する前に入力を検証します。

#[post("/snippets/", name = "snippets-create", pre_validate = true)]

ハンドラーは、検証を通過したフィールドからSnippetを組み立てます。続いて、そのモデルのManagerが、渡された接続を使ってデータベースに書き込みます。

let created = Manager::<Snippet>::new()
    .create_with_conn(&db, &snippet)
    .await?;

SnippetSerializerでは、code が空でないことなど、入力の長さに関する制約を定義しています。SnippetResponse::from_model が応答用のフィールドを取り出し、ハンドラーがJSONに変換して返します。

更新処理の views.rs::update では、serializer.validate()? を明示的に呼んでいます。このバージョンでは、作成と更新で検証の仕組みが異なります。空の code に対して示した結果は、POST による作成処理を試したものです。

確認した範囲

2026-09-05 に、新たにクローンしたリポジトリが冒頭のコミットと一致することを確認しました。本文のコマンドを使い、設定ファイルの作成とインフラの起動、3件のマイグレーション の適用に成功しています。続いて、記載どおりのCargoコマンドでHTTPサーバーを起動し、8件のcurlリクエストを実行しました。IDには、作成時の応答に含まれる値を使っています。

curlで送信したリクエスト確認できた結果
初期一覧200
作成201、title・code・languageが一致
作成したIDの取得200、フィールドが一致
更新200、更新したフィールドが一致
削除204、本文なし
削除したIDの取得404、{"error":"Snippet not found"}
codeを空文字列にして作成400
最後の一覧200、{"snippets":[]}

これとは別に、初回の検証では、サンプルに付属するBrunoの Snippets CRUDValidation Tests を実行し、8リクエスト・12テストがすべて成功しました。追加で、title を省略したPOSTも試したところ、400 が返りました。このリクエストは、上記8件のcurlには含まれていません。

ここで確認したのは、ネイティブ環境で動作する、関数ベースのRESTサンプルです。フレームワークの全機能を検証したものではありません。Pagesはまだ開発中のため、本番利用は推奨していません。Pagesについては、今回とは別に検証範囲を決めて進めます。

後片付けとフィードバック

最初のターミナルで Ctrl-C を押してサーバーを止めます。そのままサンプルのディレクトリで、次のコマンドを実行してください。

bash scripts/infra_down.sh

PostgreSQLとRedisのコンテナが停止し、保存したデータも削除されます。

サンプルが動いたら、一つずつ変更を加えて、動きの違いを確かめてみてください。使い方の質問はGitHub Discussionsへ。再現できる不具合はIssueフォームから、使用したバージョンまたはコミット、OSとRustのバージョン、実行コマンド、期待する動作と実際の結果を添えて報告してください。どの手順でつまずいたかを教えてもらえると、導入手順の改善に役立ちます。

執筆について:この記事はLLMの支援を受けて作成しました。内容は対象タグのソースと、本文に記載した実行結果に照らして確認しています。