はじめに
こんにちは、サーバーサイドエンジニアの小林です!普段はRuby on Railsを中心に開発を進めていますが、最近Rustに挑戦し始めました。Rust歴はまだ一ヶ月半ほどですが、所有権や借用といった独特な概念に苦戦しながらも、少しずつ書けるようになってきました。
しかし、Rustを実際にwebアプリケーション開発へ取り込むとなると「どう書くのがRustらしいのか」「効率よく、拡張性ある形でコードベースを継続させるには?」といった疑問が浮かんできます。
そこで、「ただ動けば良い」ではなく、Rustのベストプラクティスを知りたいと思い、「Awesome Rust」に目を向けました。
このリポジトリには、さまざまな分野のRustプロジェクトがまとまっており、非常に参考になります。
今回は、Awesome Rustの中からWebプログラミングに関連するプロジェクトを調査し、得られた知見を共有します。Rust初学者がWebアプリケーションを開発する際の参考になれば幸いです。
1. revoltchat/backend
Revoltとは
Revoltはオープンソースのチャットプラットフォームで、ユーザーファーストな体験を重視して作られています。そのバックエンドはRustで書かれており、revoltchat/backendリポジトリでその実装を見ることができます。このリポジトリにはスタイルガイドが存在し、プロジェクト全体でのコードの書き方や規約が説明されています。ここでは、Rustでwebバックエンドを書いていく上で参考になる部分を抜粋してみましょう。
ライセンス
Revoltのバックエンドは、 GNU Affero General Public License v3.0 (AGPL-3.0) の下でライセンスされています。
1.1 構造体のコメント
スタイルガイドには、「全ての構造体定義にはコメントを付けること」というルールがあります。All struct definitions must be commented.
/// Server pub struct Server { /// Name of the server pub name: String, }このように、構造体やそのフィールドに対して簡潔なコメントを付けることが推奨されています。
なぜコメントが重要なのか?
- コードの可読性向上
フィールドの意味や役割が一目で分かるため、新しい開発者がコードを理解しやすくなります。特にname
やvalue
など、一般的な名称が使われる場合は、文脈がなければその意味を誤解する恐れがあります。 - ドキュメント生成の自動化
Rustでは、///
で始まるドキュメンテーションコメントを使用することで、自動的にrustdoc
によるHTML形式のAPIドキュメントが生成できます。プロジェクトのドキュメントが整備されることで、チーム内外での情報共有がスムーズになります。
1.2 カスタムマクロで構造体の属性を簡潔に
Rustの特徴の一つに、#[derive(...)]
アトリビュートを使ったトレイトの自動実装があります。ただし、これを構造体ごとに手動で記述すると、コードが冗長になる可能性があります。
Revoltのリポジトリでは、カスタムマクロを活用してこの問題を解決しています。
通常の記述例
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Server { pub id: String, pub name: String, }
カスタムマクロを利用した記述例
auto_derived! { /// サーバー pub struct Server { pub id: String, pub name: String, } }
カスタムマクロの定義
以下のようなカスタムマクロが用意されています。macro_rules! auto_derived { ( $( $item:item )+ ) => { $( #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] $item )+ }; }このようにマクロを使うことで、繰り返し作業を減らし、一貫性を保ちながらメンテナンス性の高いコードを書くことができます。
メリット
- コードの簡潔さ
手動での記述を減らし、見た目がすっきりします。 - メンテナンス性の向上
カスタムマクロを修正するだけで、すべての構造体に反映されるため、仕様変更にも柔軟に対応可能です。
1.3 シリアライズ条件を統一する
特殊な条件でシリアライズをスキップしたい場合、既存のヘルパー関数を活用することが推奨されています。If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
#[serde(skip_serializing_if = "crate::if_false", default)]引用されたスタイルガイドでは、
if_false
という関数を使用して、false
の場合にactive
フィールドをスキップする方法が推奨されています。条件付きシリアライズをヘルパー関数にまとめることで、コードが簡潔になり、再利用性と一貫性が向上します。
公式ドキュメントも参考に
条件付きシリアライズについては、Serde公式ドキュメントにも詳しく説明されています。他にもいろいろなものが用意されているので、確認してみるのも良いかもしれません。
- 例えば、
None
である場合、Option
をスキップする設定などは、よく使うかもしれないですね。
#[serde(skip_serializing_if = "Option::is_none")]
1.4 implブロックの整理とルール
Rustでは、構造体や列挙型に関連するメソッドや関数を定義するために、impl
ブロックを使用します。Revoltのスタイルガイドには、impl
ブロックの記述に関していくつかの指針が示されています。
impl
blocks may be defined below the struct definitions and should be ordered in the same order of definition. Methods in the block must follow the same guidelines as traits where-in: methods are ordered in terms of CRUD, there are empty line breaks, and methods are commented.
ガイドラインに基づくポイント
impl
ブロックの位置impl
ブロックは、対応する構造体や列挙型の直下に配置することで、コード全体の構造が分かりやすくなります。- メソッドの順序
メソッドはCRUD(Create, Read, Update, Delete)の順序で記述することで、用途が整理され、他の開発者がコードを理解しやすくなります。 - 空行とコメント
メソッド間に空行を挿入して視覚的な区切りを作り、各メソッドにコメントを付けることで、コードの意図を明確に伝えることができます。
整理されたimpl
ブロックの例
以下に、ガイドラインに従ったimpl
ブロックの例を示します。
impl Server { /// サーバー作成 pub fn create() -> Self { /* 実装 */ } /// サーバー情報取得 pub fn read(&self) { /* 実装 */ } /// サーバー情報更新 pub fn update(&mut self) { /* 実装 */ } /// サーバー削除 pub fn delete(self) { /* 実装 */ } }
メリット
- 可読性の向上
順序や区切りが明確になることで、コードの全体像が把握しやすくなります。 - メンテナンス性の向上
一貫したスタイルで記述されているため、新しいメソッドを追加する際も迷いがありません。
このようなルールを守ることで、チームでの開発やコードの長期的な保守がスムーズになります。
また、これらのルールをlinterで設定できれば、自動的にコードスタイルをチェックできるので、レビューのコストも下げられて効率化できそうですね。
2. LemmyNet/lemmy
Lemmyとは
ここからは、Awesome Rustリポジトリに掲載されている別の例として、分散ソーシャルメディアプラットフォームであるLemmyのバックエンド実装を見てみましょう。Lemmyは、Redditに似た機能を持つオープンソースの分散型ソーシャルニュースプラットフォームで、Rustで実装されています。
この章では、LemmyNet/lemmyの構成から、Rustで大規模なWebアプリケーションを設計・構築する際のポイントを見ていきたいと思います。
ライセンス情報
Lemmyは、GNU Affero General Public License v3.0 (AGPL-3.0) の下でライセンスされています。
2.1 モノレポ構成
以下は、Lemmyのリポジトリ構造の概要です。一部を抜粋してます。lemmy/ ├── src/ │ ├── api_routes_v3.rs │ ├── api_routes_v4.rs │ ├── code_migrations.rs │ ├── lib.rs │ ├── main.rs │ ├── prometheus_metrics.rs │ ├── scheduled_tasks.rs │ └── session_middleware.rs └── crates/ ├── api/ ├── api_common/ ├── api_crud/ ├── apub/ ├── db_perf/ ├── db_schema/ ├── db_views/ ├── db_views_actor/ ├── db_views_moderator/ ├── federate/ ├── routes/ └── utils/Lemmyのリポジトリを覗くと、複数のRustクレートを1つのリポジトリに集約した「モノレポ」構成を取っていることが分かります。
src/
ディレクトリにアプリケーション本体やルートエントリーポイントが置かれ、crates/
ディレクトリ配下にはapi/
、api_common/
、federate/
、utils/
といった、特定の機能や責任を担うクレートが整理されています。
このアプローチにより、プロジェクト全体の整合性を保ちながら、各コンポーネントを独立して開発・テストすることが可能です。
特徴と利点
- 依存関係管理の一元化:
- すべてのクレートが同一リポジトリ内にあるため、互いの依存関係やバージョン調整が容易です。
- コードの一貫性と再利用性向上:
- 共通機能は
api_common/
やutils/
といった専用クレートにまとめることで、重複を削減し、プロジェクト全体で統一的な実装を保つことが可能です。
- 共通機能は
- モジュール間の明確な責務分割:
api/
クレート: APIエンドポイント関連の実装apub/
、federate/
クレート: Fediverse対応や外部連携機能db_schema/
、db_perf/
、db_views/
: データベーススキーマや最適化、ビュー機能の分離
- 機能ごとの独立開発・テスト:
- 各クレートは基本的に独立して開発・テストできるため、新機能追加やリファクタリングがプロジェクト全体に影響しにくいです。
このようなモノレポ構造は、大規模プロジェクトの効率的な管理に非常に適しています。
おわりに
今回はAwesome Rust
の中からrevoltchat/backend
とLemmyNet/lemmy
を例に、RustでのWebバックエンド構築や大規模アプリケーション設計について紹介しました。
- 構造体のコメントやカスタムマクロを活用してコードの一貫性を保つこと
- データのシリアライズルールを統一し、データ構造の設計意図を明確にすること
- モノレポ構造やクレート間の依存関係を整理し、大規模プロジェクトを効率的に管理すること
これらの手法が、RustでWebアプリケーションを作る際のヒントになれば幸いです。これからもプロジェクトやOSSのコードを読みながら学びを深め、新たな知見を皆さんと共有していければと思います。
ドリコムでは一緒に働くメンバーを募集しています!募集一覧はコチラを御覧ください!