Claude Media
Claude CodeでTypeSpecからOpenAPIを出力する — .tspだけを直させる運用

Claude CodeでTypeSpecからOpenAPIを出力する — .tspだけを直させる運用

TypeSpecでAPIを設計し、tsp compileでOpenAPIを出す構成をClaude Codeに任せる手順です。編集するのは.tspだけにし、出力側のdeny設定とmodel・op・@routeの書き方の規約をスキルに置きます。

TypeSpecでAPIを設計し、OpenAPIはコンパイラーに出させる構成なら、Claude Codeに触らせるファイルは.tspだけで済みます。openapi.yamlは出力物なので、直したくなったときも直す先はいつも.tspです。手でYAMLを直すと、次のtsp compileで消えます。

この記事では、プロジェクトの最小構成、tspconfig.yamlでの出力の固定、出力側へのEditをdenyする設定を順に組みます。Claudeに渡す規約はmodel・op・@routeの3つに絞り、スキルとして置きます。OpenAPIを直接書く場合の検証はSpectralでOpenAPIをlintする構成、OpenAPIから先のコード生成はOpenAPI Generatorの再生成運用が担当です。ここはその手前、仕様を作る段を扱います。

直す場所を3層に分ける

TypeSpecのプロジェクトには、役割の違うファイルが並びます。tsp initのGeneric REST APIテンプレートで作ると、main.tsp、tspconfig.yaml、package.json、node_modules/、tsp-output/ができます。このうちtsp-output/の下に、OpenAPIのopenapi.yamlが出力されます。

層置き場所Claude Codeの扱い
設計置き場所main.tspと.tspファイルClaude Codeの扱い編集する(これが唯一の仕様)
コンパイル設定置き場所tspconfig.yamlClaude Codeの扱い変更はレビュー対象
出力置き場所tsp-output/以下、または固定した出力先Claude Codeの扱い編集禁止。読むだけ

main.tspはビルドの入口で、モデルとサービスと操作の定義を置きます。tspconfig.yamlはコンパイラーの設定です。出力ディレクトリのopenapi.yamlは「生成された仕様」で、形式はtspconfig.yamlの指定で変わります。

最小プロジェクトを作ってコンパイルする

TypeSpecの導入はNode.js版と、実験段階のスタンドアロン版があります。Node.js版なら、Node.js LTSを入れてCLIをグローバルに入れます。

npm install -g @typespec/compiler
tsp init
tsp compile .

tsp initは対話式で、テンプレートはGeneric REST APIを選び、@typespec/httpと@typespec/openapi3のライブラリーにチェックを入れます。tsp compile .が出力を作ります。保存のたびに自動で回したいときはtsp compile . --watchも使えます。

別のマシンやCIでプロジェクトを開いたときは、tsp installで依存ライブラリーを入れ直します。書き方を揃えたいときは、tsp formatでTypeSpecのファイルを整形できます。スキルの手順にtsp formatを入れておけば、Claudeが書いた.tspの体裁も揃います。

最小のmain.tspは次の形です。公式の入門にある書き方に沿っています。

import "@typespec/http";
using Http;
 
@service(#{ title: "Pet Store" })
@server("https://example.com", "Single server endpoint")
namespace PetStore;
 
model Pet {
  id: int32;
  @minLength(1)
  name: string;
  @minValue(0)
  @maxValue(100)
  age: int32;
}

modelがOpenAPIのスキーマに、@minLengthなどのデコレーターがバリデーション項目に対応します。この対応を知っていれば、Claudeが出力を読んで.tspのどこを直すか判断できます。

tspconfig.yamlで出力の形を固定する

出力先と形式は、Claudeに口頭で伝えるよりtspconfig.yamlに書いたほうが確実です。OpenAPIエミッターはemitで指定し、設定はoptionsの下にエミッター名で書きます。

# tspconfig.yaml
emit:
  - "@typespec/openapi3"
options:
  "@typespec/openapi3":
    emitter-output-dir: "{cwd}/openapi"
    file-type: yaml
    openapi-versions:
      - "3.1.0"

押さえておく項目は3つです。

  • emitter-output-dir: 既定は{output-dir}/@typespec/openapi3です。出力先をopenapi/のような固定の場所に寄せると、denyのパスも1本で済みます
  • file-type: yamlとjsonのどちらか、または両方を配列で渡せます
  • openapi-versions: 既定は3.0.0です。下流のツールが3.1を要求するなら、ここで明示します

openapi-versionsには3.2.0も選択肢として載っています。ただし読む側のツールが対応しているかは別問題なので、版を上げる前に下流のツールの対応状況を確かめます。

model・op・@routeの書き方を規約にする

Claudeが.tspを書くときにぶれやすいのは、HTTPの動詞と、パスの組み立てと、エラー応答です。TypeSpecの既定の挙動を知らないまま書かせると、意図と違うOpenAPIが出ます。

動詞は既定で決まります。@postが付いていない操作は、リクエストボディがあれば@post、なければ@getになります。ボディを足しただけで動詞が変わるので、動詞は毎回明示させるほうが安全です。

@routeは操作やインターフェース、名前空間に付けられます。親に付けると、配下の操作すべてにパスの接頭辞が付きます。パスパラメーターは{petId}のように波括弧で書き、同名のプロパティに@pathを付けるのは任意です。

@route("/pets")
interface Pets {
  @get list(@query skip?: int32, @query top?: int32): Pet[];
  @get @route("{petId}") read(@path petId: int32): Pet | NotFoundResponse;
  @post create(@body pet: Pet): Pet;
}

このPetsは/petsと/pets/{petId}の操作を作ります。operationIdは、@operationIdを付けない場合、既定では親のコンテナー名と操作名から決まります(operation-id-strategyのparent-container)。外部のSDK生成がoperationIdに依存するなら、デコレーターで明示させるか、設定をexplicit-onlyにします。

これらをClaudeに伝える規約にまとめると、次のとおりです。

  1. モデルはmodelで定義し、検証は@minLength・@minValueなどで.tsp側に書く
  2. 操作はopかinterfaceの中のメソッドで定義し、@get・@postなどの動詞を必ず明示する
  3. パスは@routeで書く。共通の接頭辞はinterfaceかnamespaceに付ける
  4. エラー応答は戻り値の型で表す。説明は@doc、要約は@summaryで付ける
  5. operationIdが必要なら@operationIdを使う

OpenAPIの項目から.tspの直し先を引く

出力のYAMLに問題が見つかったとき、Claudeが最初に知りたいのは「どのデコレーターが生んだ項目か」です。公式のOpenAPI開発者向けガイドにある対応のうち、直す頻度が高いものを表にします。

OpenAPIの項目.tspでの書き方
minLength / maxLength.tspでの書き方@minLength(n) / @maxLength(n)
pattern.tspでの書き方@pattern(regex)
summary.tspでの書き方@summary
description.tspでの書き方@docまたはdocコメント(/** */)
既定のエラー応答(default).tspでの書き方戻り値の型のモデルに@error
title(サービス).tspでの書き方@service(#{ title: "..." })

ユニオン型にも癖があります。unionは既定でanyOfとして出力されます。スキーマが排他的であるとは限らないためです。oneOfにしたい場合は、@typespec/openapi3からusing OpenAPI;を取り込み、ユニオンに@oneOfを付けます。「oneOfで出ない」という指摘を受けたとき、Claudeが出力YAMLを書き換えず、この@oneOfを足せるかが運用の分かれ目です。

出力ファイル名は、サービスの数とバージョンで変わる

下流のツールにパスを渡すなら、出力ファイル名の決まり方も知っておく必要があります。output-fileの既定値は{service-name-if-multiple}.{version}.openapi.yamlで、file-typeがjsonなら拡張子が.jsonになります。サービスが1つでバージョン管理もない構成ならopenapi.yamlです。

サービスを複数持つとopenapi.Org1.Service1.yamlのようにサービス名が入り、バージョン管理をするとopenapi.v1.yaml・openapi.v2.yamlのように版が入ります。構成を変えたら、Spectralやコード生成に渡すパスも変わります。Claudeに「サービスを分割して」と頼む前に、下流のコマンドのパスをどこで一元管理しているかを確かめておくと、出力名の変更に気づかないまま下流が古いファイルを読み続ける事態を避けられます。出力名を固定したいなら、output-fileに固定の名前を書きます。

規約はスキルに置き、触る範囲だけで読み込ませる

CLAUDE.mdは全セッションで読み込まれ、常にコンテキストを使います。TypeSpecの規約は.tspを触るときにしか要らないので、スキルに置く構成が合います。スキルのfrontmatterには、pathsでglobを指定できます。指定すると、そのパターンに合うファイルを扱うときだけ自動で読み込まれます。

---
name: typespec-api
description: TypeSpecでAPIを設計・変更するときの規約。.tspの編集とOpenAPI出力に使う
paths: "**/*.tsp"
---
 
OpenAPIは `npm run gen:openapi` で出力する。`.tsp` だけを編集し、
`openapi/` 配下は編集しない。
 
- モデルは `model`、検証は `@minLength` 等のデコレーターで書く
- 操作は動詞デコレーター(`@get` / `@post` など)を必ず明示する
- パスは `@route`。共通の接頭辞は `interface` に付ける
- 変更後は `tsp format` で整形し、`npm run gen:openapi` を実行する
- コンパイルエラーは診断メッセージを読んで `.tsp` を直す

package.jsonのスクリプトは、コマンドを1本に固定します。

{
  "scripts": {
    "gen:openapi": "tsp compile ."
  }
}

ここで役割を分けます。「openapi/は編集しない」のような一言の禁止はCLAUDE.mdにも置いておき、細かい書き方の規約はスキルに任せます。CLAUDE.mdは文脈として読まれるだけで、強制される設定ではありません。強制は次のセクションで設定に任せます。

出力側のEditをdenyで止める

出力を手で直させない運用は、権限ルールで技術的に止めます。.claude/settings.jsonに次を置きます。

{
  "permissions": {
    "deny": [
      "Edit(/openapi/**)",
      "Edit(/tsp-output/**)"
    ],
    "allow": [
      "Bash(npm run gen:openapi)"
    ]
  }
}

先頭のスラッシュは、プロジェクト設定ではプロジェクトルートからのパスを意味します。Writeにパスを付けたルールは一致せず、書き込みも含めてEditのルールが見ます。そのためEditで書きます。

ルールの評価順はdenyが先で、denyに合えばallowでは取り消せません。ただしこのdenyが効くのは、組み込みのファイルツールと、cat・sed・teeのように認識されるコマンド、>などのリダイレクトです。Claudeが別のスクリプトを書いて、その中で出力ファイルを書き換えると、denyをすり抜けます。ファイルシステムへのアクセスをコマンドの文面に頼らず止めたい環境は、サンドボックスを併用します。

出力先をopenapi/に固定したのは、このdenyを1本で済ませるためです。出力をtsp-output/の既定のまま使うなら、denyもそちらに揃えます。

コンパイルエラーを読ませて.tspを直させる

運用の反復はこうなります。Claudeが.tspを変更し、npm run gen:openapiを実行します。エラーが出たら、診断メッセージを読んで.tspを直し、もう一度実行します。tspはエラーを色付きで整形して出す--prettyが既定で有効です。

この反復に向いているのは、エラーの直し先が常に.tspだと決まっているからです。出力YAMLの検証エラーを直す場合は、どの.tspの定義から出た項目かを辿る必要があります。デバッグ用に、include-x-typespec-nameを使うと、生成されたスキーマに元のTypeSpecの型名を示すx-typespec-name拡張を付けられます。公式はこの拡張をデバッグ用と位置づけており、下流のツールが依存する前提にしないよう注意しています。

つまずきやすい点

使っていない型も出力される: 既定では、サービスの名前空間で宣言した型はすべて出力に入ります。操作から参照される型だけにしたいときは、omit-unreachable-typesを有効にします。

出力をコミットするか: openapi.yamlを下流のツール(Spectralやコード生成)が読むなら、出力をリポジトリに置くほうが差分レビューができます。置かない場合は、CIでtsp compileを回してから下流を実行します。どちらにしても、出力を手で編集させない点は同じです。

差分で確かめる: .tspを変えたあとにOpenAPIの差分を見ると、意図しない変更に気づけます。デコレーターの追加が思わぬ場所に効くことがあるからです。出力が下流のlintを通るかは、Spectralのlint構成で毎回確認できます。

OpenAPIの先でMCPサーバーを作る場合: 出力した仕様を使うなら、OpenAPIからMCPサーバーを生成する方法へつながります。

まとめ

TypeSpec運用の要点は、仕様が.tspの1か所にあることです。Claude Codeには.tspの編集とnpm run gen:openapiの実行だけを許し、出力ディレクトリのEditをdenyで止めます。動詞を毎回明示する規約は、ボディの有無で既定の動詞が変わる仕様を踏まえたものです。出力先とopenapi-versionsはtspconfig.yamlで固定し、下流が3.1を要求するかどうかで判断します。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. typespec.io/docs
  2. typespec.io/docs/emitters/openapi3/reference/emitter
  3. typespec.io/docs/libraries/http/operations
  4. typespec.io/docs/handbook/configuration/configuration
  5. code.claude.com/docs/en/skills
  6. code.claude.com/docs/en/permissions
  7. code.claude.com/docs/en/memory
この記事を共有:XはてブLinkedIn