支出管理開発本部で事業部横断テックリードをしている @ogugu です。
広く複雑で大規模になりつつある支出管理のアーキテクチャについて、以下の連載形式でご紹介していきます。
- (本記事) 支出管理におけるTypeSpecを中心にしたスキーマ駆動開発
- ソフトウェアアーキテクチャに基づいた自動テスト戦略と実装ガイドライン
- 支出管理におけるマイクロサービスアーキテクチャの知見
今回は、TypeSpec を中心にしたスキーマ駆動開発をご紹介します。
結論からいうと、筆者は TypeSpec について「OpenAPI からの移行コストや技術的ロックインリスクを伴わず、開発体験を向上する最高のツール」と評価しています。その理由を順にご紹介します。
TypeSpec とは
まず、TypeSpec の特徴を簡潔にまとめておきます。
- Microsoft が開発した IDL (インターフェース定義言語)
- TypeScript にインスパイアされた簡潔かつ馴染みやすい構文
- OpenAPI では表現が難しい Generics & Intersections & Unions 的表現が可能
- Emitter を実装して任意の形式に出力でき、OpenAPI と Protobuf をビルトインサポート
- VSCode Extensions が提供されていて、シンタックスハイライトやコード補完をサポート
- GitHub 上でのシンタックスハイライトも最近サポートされた
- カスタム Linter / Emitter を実装でき、Formatter は組み込みのものがある
- OpenAPI から TypeSpec への移行ツールが用意されている
OpenAPI とのスキーマの比較
実際に OpenAPI とのスキーマ例を比較してみます。
OpenAPI の場合
openapi: 3.0.0 info: title: (title) version: 0.0.0 tags: [] paths: /stores: get: operationId: Stores_list parameters: - name: filter in: query required: true schema: type: string responses: '200': description: The request has succeeded. content: application/json: schema: type: array items: $ref: '#/components/schemas/Store' /stores/{id}: get: operationId: Stores_read parameters: - name: id in: path required: true schema: $ref: '#/components/schemas/Store' responses: '200': description: The request has succeeded. content: application/json: schema: $ref: '#/components/schemas/Store' components: schemas: Address: type: object required: - street - city properties: street: type: string city: type: string Store: type: object required: - name - address properties: name: type: string address: $ref: '#/components/schemas/Address'
TypeSpec の場合
import "@typespec/http"; using TypeSpec.Http; model Store { name: string; address: Address; } model Address { street: string; city: string; } @route("/stores") interface Stores { list(@query filter: string): Store[]; read(@path id: Store): Store; }
導入の意思決定
以下は支出管理のアーキテクチャ概要です。

マイクロサービス間の通信は、主に gRPC と PubSub による連携を使い分けています。 一方、Webやモバイルなどのクライアントからは、OpenAPI をベースにした BFF へ問い合わせる形式です。
当初、直接 OpenAPI を読み書きしていましたが、メンバーからは「読み書きが辛い」という不満がありました。 yaml という標準的な表現手段は手堅い反面、冗長になるのがトレードオフです。 Coding Agent が台頭する今、書き辛さはなんとかなっても、一見の読み辛さはどうにもなりません。
また、BFF であれば GraphQL によって API 集約を表現する手段もあります。 freee で導入するにあたり、既存構成からの飛躍や社内標準との乖離が課題でした。 そこで、当時は登場して間もなかった TypeSpec に目をつけました。
この際、特に気になるのが、「成熟したエコシステムがあるか」「技術的なロックインリスクがないか」という点です。
そこは、コード生成の際、必ず OpenAPI を中間成果物として介するという導入方法 でリスクを払拭しています。
これによって、成熟した OpenAPI のエコシステムに乗れる上、TypeSpec を捨てたくなったとしても、生成結果の OpenAPI をマスターにすればよいです。 こうした理由から、明確なデメリットがない一方でメリットが大きいと判断し、導入に踏み切りました。
TypeSpec を取り入れた開発フロー

以下の流れでスキーマ定義→コード生成をしています。
- TypeSpec によってスキーマを定義
- TypeSpec に対する linter formatter の実行
- TypeSpec から OpenAPI として出力
- バックエンド (Go) は ogen でサーバー実装を生成
- フロントエンド (React) は orval で tanstack-query のカスタムフックと msw のモックサーバーを生成
先述の通り、実装生成は OpenAPI を起点にして、TypeSpec へのロックインを防いでいます。
以下のように、Generics を活用している事例もあります。 支出管理では、複数種類のマスタデータのインポート機能があり、その結果の IF を Generics を使ってまとめています。
@doc("エラー解析結果。1000件を超える場合があるので署名urlを返す") model ValidateResult<T> { @doc("エラー行数") error_rows_count: int64; @doc("エラー行情報") error_rows: PreviewRow<T>[]; @doc("エラー情報のCSV署名URL") error_csv_url: string; }
カスタム Linter / Decorator
また、TypeSpec ではカスタム Linter や Decorator を容易に実装することができます。
例えば、弊チームの @mascii は mascii/typespec-decorator-int64-as-string という Decorator を実装しています。
int64
のパラメーターに @int64AsString
というデコレーターを付与すると OpenAPI 側で type: string
format: int64
と出力されるものです。
JavaScript の Number.MAX_SAFE_INTEGER
を超える値が入りうる場合にこのような設定にすることで、フロントエンド側からは string
として、バックエンドが int64
として扱うことができています。もちろん、これは扱うツールチェーンの解釈にもよります。
また、私自身は ogugu9/typespec-route-linter という Linter を実装しました。
TypeSpec ではパスを設定するための @route
を namespace
, interface
, op
に設定できますが、 op
以外はネストが可能です。
ネストすることでルーティングが DRY になりますが、一方で TypeSpec 上の greppability は低下します。
弊チームでは多少の DRY は捨ててフラットなルーティングを推奨しているため、この Linter を作ってみました。
どちらも実装自体はシンプルに収まっており、利用者側で拡張するためのエコシステムが整っている印象でした。
導入後の状況
2025年6月時点で、導入から約1年半が経ち、現在 300 件以上の API が定義されていますが、現状クリティカルな課題は発生していません。
今では OpenAPI を直接読み書きすることは考えられなくなりました。
その間も、OpenAPI 向けの移行ツールの追加、GitHub 上でのシンタックスのサポートが完備、1.0.0 がリリースされるなど、エコシステムはむしろ進化を遂げており、開発体制としては信頼感があります。
一方で、あえて弊チームが唯一直面した課題を挙げてみます。
それは、配列・オブジェクトのパス変数・クエリパラメーターのシリアライズ形式を指定する explode
に対する解釈の問題です。
ref. microsoft/typespec/issues #7574
OpenAPI ではクエリパラメータの explode
のデフォルトが true
であるのに対して、TypeSpec では false
としています。
これは RFC 6570 の URI Template に関する仕様で定義される「explode 演算子 *
を明示的に付与しなかった場合の挙動」に合わせているとのことでした。
こういった点で OpenAPI の仕様に対する厳密な一致は目指しておらず、TypeSpec としての主張も持っていくようです。
その背景には、おそらく TypeSpec 自身も TypeSpec からのコード生成機能を有していたり、OpenAPI 以外の形式への出力も対応していることもあると推察しています。 (実際、TypeSpec のコード生成機能の内部では uri-template
を利用しています。)
今回は explode
を明示指定すればよいだけの話でしたが、こういった背景や思想を理解して利用する必要があります。
最後に
開発者体験は、メンバーにとっての直感的な開発体感が全てだと思いますが、その点で TypeSpec は最高と感じています。
すでに OpenAPI でスキーマを書くチームにとって、導入しない理由がなくなったといっても過言ではないでしょう。
この記事を通して TypeSpec がより普及し、エコシステムがさらに進化していくことを期待しています。
皆さんもぜひ導入を検討してみてください。