「Connect RPC×スキーマ駆動開発」の現場知見と設計思想
はじめに
こんにちは、GOGENでCTOをしている楠本(@zabio3)です。
GOGENでは、不動産売買取引というドメイン特有の複雑さを捉えるSaaS開発において、APIの設計・運用方針を極めて重要なテーマと捉えています。事業としてのスピードと品質の両方、フロントエンド・バックエンド間の滑らかな連携、そして複数サービス間の統合性を担保するため、私たちは Connect RPC を中核としたスキーマ駆動開発 を選択しました。
この選択には、単なる技術的な興味を超えて、将来的なスケーラビリティやメンテナンス性への投資という明確な意図があります。
本記事では、その背景にある設計思想と、どのような実務に落とし込んでいるかを解説します。
なぜConnect RPCを選んだのか
従来のRESTベースのAPIでは、以下のような問題が顕在化していました
- 型の非対称性:バックエンドはGo、フロントはTypeScriptで手動管理。整合性チェック困難
- バージョニングの煩雑さ:URLやリクエスト構造での対応が人手に依存
- API仕様書の信頼性:Swaggerなどによるドキュメントが常に最新とは限らない
これに対し、Connect RPCは以下の利点を提供します。
- Protocol Buffersベースの明確な仕様管理
- gRPC・gRPC-Web・Connectプロトコルのマルチサポート
Connect独自プロトコルは、HTTP/1.1ベースで動作し、gRPCと異なりブラウザとの互換性が高く、テキストベースの通信も可能な軽量仕様 - Bufと連携したコード生成・Lint・Breaking Changeの検出
- 型安全な開発体験(Go/TSでの自動生成)
加えて、gRPCのブラウザ非対応課題を回避しつつ、REST風の可読性と保守性を両立できる点も魅力でした。
スキーマ駆動開発の運用設計
私たちはスキーマが唯一の真実のソースという思想に基づき、開発プロセスを次のように設計しています
- protoファイル定義:新規機能や変更はまず.protoで定義。
service AnkenService {
rpc Create(AnkenCreateRequest) returns (AnkenCreateResponse) {}
}
- BufによるLint / Breaking Changeチェック:CIに組み込み、品質担保。
- buf generateによるGo/TS型の生成:connect-go, connect-web向けコードを自動生成。
サービス変更をした場合も buf generate
を実行すれば、最新のインタフェースと各種クライアントコードが自動生成される為、クライアントとサーバー間の一貫性が保たれ、バージョンアップ時の手動更新やコミュニケーションに費やすコストが省けます。
- バックエンド(Go)
service AnkenService {
rpc Create(AnkenCreateRequest) returns (AnkenCreateResponse) {}
}
func NewAnkenServiceHandler(svc AnkenServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
// 自動生成されたハンドラーコード
}
- フロントエンド(TypeScript)
export class AnkenCreateRequest extends Message<AnkenCreateRequest> {
organizationId = "";
name = "";
// その他のフィールドと型安全なメソッド
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Anken {
return new Anken().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Anken {
return new Anken().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Anken {
return new Anken().fromJsonString(jsonString, options);
}
static equals(a: Anken | PlainMessage<Anken> | undefined, b: Anken | PlainMessage<Anken> | undefined): boolean {
return proto3.util.equals(Anken, a, b);
}
}
- 共通パッケージでの型共有:フロント/バックエンド間で共通の型を参照。
- 型ベースのインターフェース定義:コードレビューの観点を仕様レビューに昇華。
結果として、僕らの少人数チームは、コミュニケーションコストを大幅に削減し、より価値のある開発タスクに時間を費やすことができるようになりました。(API更新に伴うドキュメンテーションやボイラープレートにコード修正に充てる時間を、価値の実装や既存コードの改善に充てることができる。)
効率的なエコシステムの活用
Bufのエコシステムは、型生成だけでなく周辺ツールとの連携も充実しています。たとえば、buf.gen.yamlに pseudomuto-doc をpluginとして追加することで、自動的にAPIドキュメントを生成することができます。
以下はその設定例です
plugins:
- plugin: buf.build/community/pseudomuto-doc
out: ./gen/all/docs
opt:
- markdown,docs.md
また、ConnectはgRPCと互換性があり、既存のgRPCツールを活用できます。
grpcui -protoset <(buf build ../proto -o -) -plaintext localhost:8080
また、ローカルでの開発・デバッグでは grpcui
のようなツールも活用しています。buf
と組み合わせて実行することで、.proto ファイルから直感的にAPI仕様を確認・操作できる点が気に入っています。
フロント・バック共通型生成の工夫
フロントエンドでは、@bufbuild/connectを活用してTransportレイヤーを構築し、バックエンドが提供するService定義に対応したClientを組み立てています。型補完が効くことで、開発者の学習コスト・実装ミスの低減に貢献しています。
また、API定義に関するPRはGitHub上でproto単体にレビューを集中させ、他のロジックと分離した運用を確立しています。
バージョニングと後方互換性
スキーマは service/v1/xxx.proto のようにディレクトリ単位でバージョン管理しています。これにより、v1の仕様を維持しつつ、v2への段階的移行が可能です。
また、BufのBreaking Change検出を使って、後方互換を破る変更はCIで強制ブロック。安全性と開発効率のバランスを保っています。
Interceptorの課題による自前実装
Connect RPCはgRPCと互換性があり、多くのgRPCエコシステムと連携可能ですが、Interceptor(ミドルウェア)設計においては明確な違いが存在します。特に、以下のようなgRPC向けミドルウェア群(例: go-grpc-middleware)は、Connectではそのまま利用できません。
このため、私たちは以下のような必要最小限の機能群について、ライトな構成で自前実装を行い、Connectと統合する設計を選びました。
- トレーシング(OpenTelemetry / Datadogとの連携)
- 認可処理(Authの共通層)
- ログ収集(構造化ログ)
仕様を読み解きつつ、最終的にプロダクト間で再利用可能な共通層として整理することで安定運用に至りました。
導入による変化と課題
成果
- 型のズレに起因するバグの大幅減少
- 実装スピードの向上(特にAPI連携)
- ドメインごとに関心が分離され、チーム間のレビューが効率化
課題
- protoに不慣れなフロントエンドメンバーの学習コスト
- 自動生成された型の命名や構造に対する違和感(命名規則統一)
- 共通定義の管理とスキーマの肥大化
おわりに、GOGENではエンジニアを募集しています!
Connect RPCは単なるAPI通信手段ではなく、設計思想そのものを開発基盤に反映させるための手段だと私たちは捉えています。コードや設計に刻まれた意思決定の積み重ねによって、複雑な不動産ドメインにおけるプロダクト連携・運用効率・品質担保を一気通貫で実現してきました。
本記事で紹介したような設計や仕組みは、単に「やってみた」だけではなく、実際に現場のリポジトリとコードに根差し、プロダクト価値を高めるためのトレードオフを重ねた結果です。
もし、設計とビジネス価値の接点に面白みを感じる方がいれば、ぜひ一度お話ししましょう。
採用情報の詳細は、下記リンクよりご覧いただけます👇
Discussion