このサイトのバックエンドは Rust +
Axum
で実装しています。
本番環境でリクエストを受けているわけではなく、手元のマシンでビルド時に
data/
からデータを取得するためだけに使っています。
機能面では3点の理由で選びました。
特にts-rsによる型共有は、バックエンドとフロントエンドの型の乖離を防げて便利です。
この理由のほかに
という理由もありました。
api/src/
以下は以下の4層に分かれています。
api/src/
├── main.rs # エントリポイント、ルーター設定
├── presentation/ # HTTPハンドラー層
├── app/ # アプリケーション層(サービス)
├── domain/ # ドメイン層(エンティティ定義)
└── infra/ # インフラ層(ファイル読み込み)
HTTPハンドラーです。
リクエストを受け取り、
app
層を呼び出してレスポンスを返します。
pub async fn get_tech_posts() -> Json<TechPostsResponse> {
let posts = fetch_tech_posts().await.unwrap_or_default();
Json(TechPostsResponse { posts })
}
ビジネスロジックをまとめています。
infra
層を呼び出してデータを取得し、加工してレスポンス用の構造体に変換します。
エンティティの型定義が置かれます。
#[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>,
}
data/
以下の YAML・Markdown ファイルを読み込んで
domain
層の型に変換します。
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))
Axum のシンプルなルーターAPIとRustの型安全性を活かし、
presentation
→
app
→
domain
→
infra
の4層構造で実装しています。