Rust / Axum によるAPIサーバー

このサイトのバックエンドは Rust + Axum で実装しています。
本番環境でリクエストを受けているわけではなく、手元のマシンでビルド時に data/ からデータを取得するためだけに使っています。

1. なぜ Rust か

機能面では3点の理由で選びました。

  • 型安全 : コンパイル時に多くのバグを検出できる
  • 高速 : 大量のファイルを読み込む処理がある
  • ts-rs : Rustの型定義からTypeScriptの型を自動生成できる

特にts-rsによる型共有は、バックエンドとフロントエンドの型の乖離を防げて便利です。

この理由のほかに

  • Rustで何か作りたかったから

という理由もありました。

2. ディレクトリ構成

api/src/ 以下は以下の4層に分かれています。

api/src/
├── main.rs           # エントリポイント、ルーター設定
├── presentation/     # HTTPハンドラー層
├── app/              # アプリケーション層(サービス)
├── domain/           # ドメイン層(エンティティ定義)
└── infra/            # インフラ層(ファイル読み込み)

3. 各層の責務

3.1 presentation層

HTTPハンドラーです。
リクエストを受け取り、 app 層を呼び出してレスポンスを返します。

pub async fn get_tech_posts() -> Json<TechPostsResponse> {
    let posts = fetch_tech_posts().await.unwrap_or_default();
    Json(TechPostsResponse { posts })
}

3.2 app層

ビジネスロジックをまとめています。
infra 層を呼び出してデータを取得し、加工してレスポンス用の構造体に変換します。

3.3 domain層

エンティティの型定義が置かれます。
#[derive(Serialize, TS)] を使い、Rustの型からTypeScriptの型を自動生成します。

#[derive(Debug, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TechPostFrontMatter {
    pub id: u32,
    pub name: String,
    pub title: String,
    pub pub_date: String,
    pub tags: Vec<TagLink>,
}

3.4 infra層

data/ 以下の YAML・Markdown ファイルを読み込んで domain 層の型に変換します。

4. ルーター設定

main.rs でルートを一覧定義しています。
記事・ブログ・技術記事・観察記録・用語集・壁紙など、サイトのすべてのコンテンツに対応するエンドポイントがあります。

let app = Router::new()
    .route("/articles", get(handlers::get_articles))
    .route("/articles/:slug", get(handlers::get_article_detail))
    .route("/tech", get(handlers::get_tech_posts))
    .route("/tech/:slug", get(handlers::get_tech_post_detail))

5. まとめ

Axum のシンプルなルーターAPIとRustの型安全性を活かし、 presentation app domain infra の4層構造で実装しています。