支出管理開発本部で事業部横断テックリードをしている @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 がより普及し、エコシステムがさらに進化していくことを期待しています。
皆さんもぜひ導入を検討してみてください。
