Upgrade to Pro — share decks privately, control downloads, hide ads and more …

令和最新技術で伝統掲示板を再構築: HonoX で作る型安全なスレッドフロート型掲示板 / か...

Avatar for calloc134 calloc134
October 17, 2025

令和最新技術で伝統掲示板を再構築: HonoX で作る型安全なスレッドフロート型掲示板 / かろっく@calloc134 - Hono Conference 2025

Hono Conference 2025 で発表するスライドです。

Avatar for calloc134

calloc134

October 17, 2025
Tweet

More Decks by calloc134

Other Decks in Research

Transcript

  1. 自己紹介 かろっく @calloc134 ソフトウェアエンジニア志望 (26卒) TypeScript や Rust を主に利用 アプリケーション領域

    (フロントエンド・バックエンド) を主に担当 データ基盤・インフラ・セキュリティなど幅広く・・・ 最近は新しい技術の探究、コードリーディング(React)・RFC読解(OAuth/OIDC)に注力 GitHub | Twitter | Zenn 2
  2. 本日のアジェンダ 1. The "Why": なぜVakKarmaを作ったのか? (動機と技術選定) 2. The "How": どうやって作ったのか?

    (型安全性とDDD) 3. The "What": 何ができたのか? (機能とデモ) 4. The "Next Steps": 今後の展望と学び 4
  3. HonoX サンプルコード return c.render( // ... <ul className="flex flex-wrap gap-4">

    {threadTop30.map((thread, index) => ( <li key={thread.id.val} className="flex-none max-w-md"> <a className="text-purple-600 underline whitespace-normal break-words" href={ index < 10 ? `#thread-${thread.id.val}` : `/threads/${thread.id.val}/l50` } > {index + 1}: {thread.title.val} ({thread.countResponse}) </a> </li> ))} </ul>
  4. SafeQL サンプルコード const result = await sql<{ id: string }[]>`

    INSERT INTO threads( id, title, posted_at, updated_at, epoch_id ) VALUES( ${thread.id.val}::uuid, ${thread.title.val}, ${thread.postedAt.val}, ${thread.updatedAt.val}, ${thread.epochId.val} ) RETURNING id `; 12
  5. 比較: 伝統的BBS vs VakKarma 項目 伝統的なBBS (例: Perl/PHP) VakKarma UIレンダリング

    文字列ベースのテンプレート 型安全なJSX (HonoX) DBクエリ 手動でのSQL文字列組み立て レスポンスが型安全となったSQL (SafeQL) アーキテクチャ 基本はハンドラにベタ書き 責務やレイヤの分割されたDDD エラー検出 多くが実行時エラー コンパイル時 / リント時エラー 14
  6. 設計思想の柱: ドメイン駆動設計(DDD) ドメイン駆動設計(DDD) とは? ソフトウェアで解決したい領域(ドメイン) のモデリングに焦点を当てる設計アプローチ VakKarmaにおけるドメイン 「スレッド」 「レス」 「トリップ」

    「ID」といった掲示板を定義する概念やルール 目的 ドメインの概念をコードに正しく落とし込み、ルールを散逸させない べた書きのコードと比べ、 何を達成しているかが明確なコード を実現 集約外の情報を取得したいときは高階関数パターンを利用 細かい手法にはこだわらず柔軟に、DDDの恩恵を十分に享受できるよう 戦略を優先させる 17
  7. DDD サンプルコード: メールアドレス(1) // メールアドレス export type WriteMail = {

    readonly _type: "WriteMail"; readonly val: string; }; // https://zenn.dev/igz0/articles/email-validation-regex-best-practices const regexMail = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; 18
  8. DDD サンプルコード: メールアドレス(2) export const createWriteMail = ( value: string

    | null ): Result<WriteMail, ValidationError> => { if (value === null) { return ok({ _type: "WriteMail", val: "" }); } if (value.length > 255) { return err(new ValidationError("メールアドレスは255文字以内です")); } // 簡単なメールアドレス形式チェック (厳密なものではない) if ( value !== "" && value.toLowerCase() !== "sage" && !regexMail.test(value) ) { return err(new ValidationError("不正なメールアドレス形式です")); }
  9. DDD サンプルコード: 投稿者名(1) type SomeWriteAuthorName = { readonly _type: "some";

    readonly authorName: string; readonly trip: string; }; type NoneWriteAuthorName = { readonly _type: "none"; readonly authorName: string; }; // 投稿者名 export type WriteAuthorName = { readonly _type: "WriteAuthorName"; // readonly val: string; // some/noneパターン readonly val: SomeWriteAuthorName | NoneWriteAuthorName; }; 20
  10. DDD サンプルコード: 投稿者名(2) export const createWriteAuthorName = async ( authorName:

    string | null, // 高階関数パターンで、より低レイヤの処理を隠蔽できるようにする getDefaultAuthorName: () => Promise<Result<string, Error>> ): Promise<Result<WriteAuthorName, ValidationError>> => { if (!authorName) { const nanashiName = await getDefaultAuthorName(); if (nanashiName.isErr()) { return err(nanashiName.error); } return ok({ _type: "WriteAuthorName", val: { _type: "none", authorName: nanashiName.value, }, }); }
  11. DDD サンプルコード: 投稿者名(3) // ... 続き if (authorName.includes("#")) { const

    [name, tripKey] = authorName.split("#"); const trip = createTrip(tripKey); return ok({ _type: "WriteAuthorName", val: { _type: "some", authorName: name, trip: trip, }, }); } return ok({ _type: "WriteAuthorName", val: { _type: "none", authorName, },
  12. DDD サンプルコード: ディレクトリ構造 (1) ├── conversation // 掲示板のコアに関する関心事 │ ├──

    domain │ ├── repositories │ └── usecases ├── config // 設定ファイルの関心事 │ ├── domain │ ├── repositories │ └── usecases └── shared // 共通のコード ├── types // 型定義やエラー定義 └── utils // 汎用的なユーティリティ関数 24
  13. DDD サンプルコード: ディレクトリ構造 (2) ├── conversation │ ├── domain │

    │ ├── read // 読み込み系ドメイン 内部からの信頼できるオブジェクト │ │ │ ├── ReadAuthorName.ts │ │ │ └── ... │ │ └── write // 書き込み系ドメイン 外部からの信頼できないオブジェクト │ │ ├── WriteAuthorName.test.ts │ │ ├── WriteAuthorName.ts │ │ └── ... │ ├── repositories // リポジトリ層 │ │ ├── createResponseByThreadIdRepository.ts │ │ ├── getAllThreadsRepository.ts │ │ └── ... │ └── usecases // ユースケース層 │ ├── getTopPageUsecase.ts │ ├── postResponseByThreadIdUsecase.ts │ └── ... └── ... 25
  14. DDDの実践: コード品質を高める工夫 neverthrow によるエラーハンドリング 例外ではなく Result 型を返す設計の採用 エラーと例外を分離する考え方 Tagged Union

    パターン を利用したドメインオブジェクト定義 string 型ではなく独自の UserName 型などを定義 異なる型の値の意図しない混同を、型レベル設計で防止 DBロジックの完全な隔離 SQLクエリはリポジトリ層に完全に封じ込め、システムのモジュール性を向上 26