本文へ移動
Contracts · One source of truth

型と契約の共有Contracts

松葉は複数のアプリ・複数の言語に分かれています。アプリ同士がやり取りするとき、「やり取りするデータの形(型)」が双方でずれていると不具合の温床になります。たとえば送り手と受け手で項目名が 1 文字違うだけで、データが正しく渡りません。これを「気をつける」ではなく「構造的に起こらない」状態にするため、1 つの定義から各言語の型を自動生成し、画面とサーバの間も共通の検証ルールで繋ぎました。

実装済み: 型を生む定義と共有ルール 今後: 各イベント・API の中身を実装
01

1 つの定義から、2 言語の型を生むGenerate, don't duplicate

アプリ間でやり取りする「出来事(イベント)」の形は、protobuf(プロトバフ。データ構造を言語に依存しない形で記述する仕組み)で 1 か所に定義します。そこから Buf という道具で、TypeScript と Go の型を機械的に生成します。人が両方を手書きしないので、コピー忘れや書き間違いによるズレが起きません。

図 01 · 1 定義 → TypeScript(送り手)+ Go(受け手)を自動生成
1 つの定義(真実の場所)protobuf 定義4 ファイル / 13 イベントfile · folder · share · boxBuf で生成pnpm proto:genTypeScript の型送り手 = apps/api(イベントを発行する側)packages/events/srcGo の型受け手 = apps/sync・apps/scanner(受け取る側)packages/events/gen/go送り手(TypeScript)と受け手(Go)が、同じ 1 つの定義から生まれた型を使う = 構造的にズレない。

ワイヤ(実際に流れるメッセージ)は JSON です。 protobuf は「メッセージをバイナリで流すため」ではなく、型の自動生成とスキーマ(形)の統制のために使っています。流れるデータ自体は読みやすい JSON 形式なので、運用時に中身を目視で確認できます。

このアプリでの例「ファイルがアップロードされた(file.uploaded)」というイベントの形を 1 か所で定義しておくと、それを発行する側(業務 API・TypeScript)と受け取る側(検査・同期・Go)が同じ型を使います。後から項目を 1 つ増やしても、両言語の型が同時に更新されるため、片方だけ古いという状態が生まれません。
02

定義した 13 のイベントThirteen events

現時点で 13 種類のイベントを定義しました。4 つの定義ファイル(ファイル系・フォルダ系・共有系・Box 系)に分けています。名前({資源}.{動作} の形)と、それが業務上どんな出来事を表すかは次のとおりです。

イベント名業務上の出来事
ファイル系 — file.proto(6 種)
file.uploadedファイルのアップロードが完了した(記録・追跡用)
file.scan_cleanウイルス検査で問題なしと判定。ここから Drive 同期が始まる
file.scan_infected感染を検出。隔離し、管理者へ通知
file.replacedファイルが新しい版に差し替えられた
file.deleted_softファイルが(ゴミ箱へ)論理削除された
file.deleted_hard保持期間を過ぎ、物理的に削除された
フォルダ系 — folder.proto(4 種)
folder.createdフォルダが作成された
folder.renamedフォルダ名が変更された
folder.deleted_softフォルダが論理削除された
folder.deleted_hardフォルダが物理削除された
共有系 — share.proto(2 種)
share.created共有リンクが発行された(受取人へ案内メール)
share.accessed共有リンクがアクセスされた(監査記録用)
Box 系 — box.proto(1 種)
box.drive_sync_toggledBox の Drive 同期設定が ON / OFF された

イベント名はシステムの「共通言語」です: これら 13 個の名前は、開発・運用・障害調査のすべてで使う共通の語彙になります。命名を {資源}.{動作} の形に統一し、1 か所のリストで管理することで、「人によって呼び方が違う」混乱を防いでいます。各イベントが実際にどう流れて処理されるかは「イベント駆動の整合性」「ユースケース別ワークフロー」で扱います。

03

画面とサーバも、同じルールで繋ぐFront meets back

アプリ間だけでなく、画面(ブラウザ側)とサーバ(API 側)の間でも、同じ型ズレの問題が起きます。松葉では @repo/shared という共通部品に、両者が使う検証ルール(Zod スキーマ。入力値が正しい形かを判定するルール)・定数・型を集約し、唯一の置き場としました。

このアプリでの例「アップロードできるファイルは 1 つ 200 MB まで」という上限を @repo/shared に 1 つ定義しておくと、画面側の入力チェックもサーバ側の受け取りチェックも同じ値を参照します。「画面では 200 MB、サーバでは 100 MB」のような食い違いが起きません。
置き場(サブパス)役割(業務上の意味)
entitiesユーザ・Box・ファイル・フォルダなどの「形」の定義(Zod スキーマ)
schemasAPI のリクエスト・レスポンスの検証ルール
constants業務で決まっている値(例: ファイル上限 200 MB、既定の保持期間 180 日、監査記録の種別 21 種 など)
types共通の型(API 応答・ページング・並び順など)
utils共通の小さな処理(拡張子判定・日付整形など)
errorsエラーの種類を表すコードの唯一の定義(画面とサーバで共通)

設計上のこだわり: 決まった値しか取らない項目(状態・種別・権限など)は、文字列で手書きせず、データベース側の定義から型を引き継ぐ仕組みにしています。これにより「データベースには無い値を、うっかりコードに書いてしまう」ミスを、書いた時点で検出できます。画面とサーバの通信は、特別な通信ライブラリを使わず、標準的な通信(fetch)に薄いラッパーをかぶせた素直な作りにして、仕組みを単純に保っています。