<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Yoshimoto-Samonji on Rindrics Mumbles</title>
    <link>https://rindrics.com/tags/yoshimoto-samonji/</link>
    <description>Yoshimoto-Samonji</description>    
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <lastBuildDate>Sun, 12 Jul 2026 00:00:00 +0900</lastBuildDate>
    <atom:link href="https://rindrics.com/tags/yoshimoto-samonji/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>API 契約駆動で開発できるようにした</title>
      <link>https://rindrics.com/posts/stock-assess-api-contract/</link>
      <pubDate>Sun, 12 Jul 2026 00:00:00 +0900</pubDate>
      
      <guid>https://rindrics.com/posts/stock-assess-api-contract/</guid>
      <description>&lt;p&gt;Open API 形式の API 仕様を基礎として開発できるような体制を整えた。&lt;/p&gt;
&lt;h2 id=&#34;背景と問題点&#34;&gt;背景と問題点&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;../stock-assess-plumber/&#34;&gt;前回記事&lt;/a&gt;では、frasyr をモノリスの API サーバーとして動かすための第一歩として、ひとまず plumber でラップしてみた。
当然 frasyr を API 経由で使えるようになったものの、一方で後に控える契約層やフロントの開発のことを考えると、依存の向きに致命的な課題があった。
というのも、前回は plumber をひとまず触ってみるのが目的だったので、frasyr を API 化できること、また API ドキュメントを Swagger UI で閲覧できることを確認して一区切りとしていた。
問題は API ドキュメントの確認のほうで、これをやりたいために前記事では plumber の &lt;code&gt;getApiSpec()&lt;/code&gt; を利用しており、将来的には契約の実体としたい OpenAPI 形式の仕様ファイルの方がコード&lt;strong&gt;から生成&lt;/strong&gt;されていた。
本来あるべき姿としては、モノリスの API は契約&lt;strong&gt;に準拠&lt;/strong&gt;している必要があるが（下図左）、API から仕様を生成したのでは、依存の向きが逆になってしまっている（下図右）。
これでは「契約」という概念を持ち込んだ意味がない。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&#34;https://rindrics.com/images/2026-07-12-api-contract/generated-contract.webp&#34;
    alt=&#34;前回記事の時点では、契約のほうが API から生成されてしまっていた&#34;&gt;&lt;figcaption&gt;
      &lt;p&gt;前回記事の時点では、契約のほうが API から生成されてしまっていた&lt;/p&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;将来的には上図左の &amp;ldquo;Contract layer&amp;rdquo; を契約から生成したいので、次のステップとして下記 2 点に取り組むこととした:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open API 形式の API 仕様ファイルを契約の SSOT とする&lt;/li&gt;
&lt;li&gt;前回作ったモノリスサービスがこの契約に違反しないような仕組みを確立する&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;やったこと&#34;&gt;やったこと&lt;/h2&gt;
&lt;p&gt;ざっくりこんな感じ:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 仕様をバージョン管理&lt;/li&gt;
&lt;li&gt;Contract test の導入&lt;/li&gt;
&lt;li&gt;リクエスト・レスポンスの検証&lt;/li&gt;
&lt;li&gt;API ドキュメントの配信&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;それぞれ簡単にメモしておく。&lt;/p&gt;
&lt;h3 id=&#34;api-仕様をバージョン管理&#34;&gt;API 仕様をバージョン管理&lt;/h3&gt;
&lt;p&gt;Open API 形式の仕様ファイルを、フロント&amp;ndash;バック間における契約の SSOT としたい。
これまでは plumber から生成される &lt;code&gt;openapi.json&lt;/code&gt; を &lt;code&gt;.gitignore&lt;/code&gt; していたがこれをやめ、バージョン管理を始めた。&lt;/p&gt;
&lt;p&gt;これから仕様を手で書いていくとなると JSON 形式だと少々厳しそうなので、ついでに YAML に変換した:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;yq -p json -o yaml &lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#39;.&amp;#39;&lt;/span&gt; schema/openapi.json &amp;gt; schema/openapi.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;contract-test-の導入&#34;&gt;Contract test の導入&lt;/h3&gt;
&lt;p&gt;frasyr の API サーバーを契約通りに実装できているかを確認するために Contract test を導入した。
テスト用ツールには当初 Claude に薦められるがままに &lt;a href=&#34;https://dredd.org/en/latest/&#34;&gt;Dredd&lt;/a&gt; を使いかけていたが、&lt;a href=&#34;https://github.com/apiaryio/dredd&#34;&gt;repo&lt;/a&gt; の方を見てみたら &lt;code&gt;public archive&lt;/code&gt; になっていたので &lt;a href=&#34;https://schemathesis.io/&#34;&gt;Schemathesis&lt;/a&gt; を使うことにした。
他の候補として一応 &lt;a href=&#34;https://stoplight.io/open-source/prism&#34;&gt;Prism&lt;/a&gt; も検討したが、一人で開発するので Prism の売りの一つであるモックサーバーはそんなに魅力的に映らなかった。&lt;/p&gt;
&lt;p&gt;Contract test としてのコア機能はこんな感じ:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Examples: API 仕様の example に valid なリクエスト例を書いておくことで、正常系のリクエストが生成される&lt;/li&gt;
&lt;li&gt;Fuzzing: API 仕様をもとに、正常系・異常系のリクエストを生成する&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Examples はまぁいいとして、Fuzzin test がなかなかおもしろかった。
Schemathesis が生成した、仕様的に valid なリクエストに対して &lt;code&gt;4XX&lt;/code&gt; や &lt;code&gt;5XX&lt;/code&gt; が返った場合、テストが失敗してくれるので、エラーハンドリングが甘かったり、仕様記述が漏れていたりといったミスに気付ける。
また逆に、仕様的に invalid なリクエストに対して &lt;code&gt;2XX&lt;/code&gt; 系が返った場合にも、エラーハンドリングや仕様記述のミスに気づけるというわけ。&lt;/p&gt;
&lt;p&gt;あとは &lt;code&gt;Reproducible Failures&lt;/code&gt; という機能が地味に便利で、期待と異なるレスポンスが返った時に、再現用の &lt;code&gt;curl&lt;/code&gt; リクエストを表示してくれた:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;=================================== FAILURES ===================================
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_________________________________ POST /v0/vpa _________________________________
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;1. Test Case ID: FZf6yc
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- Server error
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[500] Internal Server Error:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    `{&amp;#34;error&amp;#34;:&amp;#34;Internal validation error&amp;#34;}`
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Reproduce with:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    curl -X POST -H &amp;#39;Content-Type: application/json&amp;#39; -d &amp;#39;{&amp;#34;data&amp;#34;: {&amp;#34;caa_url&amp;#34;: &amp;#34;https://raw.githubusercontent.com/ichimomo/frasyr/dev/data-raw/ex2_caa.csv&amp;#34;, &amp;#34;waa_url&amp;#34;: &amp;#34;https://raw.githubusercontent.com/ichimomo/frasyr/dev/data-raw/ex2_waa.csv&amp;#34;, &amp;#34;maa_url&amp;#34;: &amp;#34;https://raw.githubusercontent.com/ichimomo/frasyr/dev/data-raw/ex2_maa.csv&amp;#34;}, &amp;#34;params&amp;#34;: {&amp;#34;m&amp;#34;: 0.5, &amp;#34;fc_year&amp;#34;: [2015, 2016, 2017], &amp;#34;tf_year&amp;#34;: [2015, 2016], &amp;#34;term_f&amp;#34;: &amp;#34;max&amp;#34;, &amp;#34;stat_tf&amp;#34;: &amp;#34;mean&amp;#34;, &amp;#34;pope&amp;#34;: true, &amp;#34;tune&amp;#34;: false, &amp;#34;p_init&amp;#34;: 0.5}}&amp;#39; http://host.docker.internal:31502/v0/vpa
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    st replay FZf6yc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;これだけでも、サーバー側のログと照らし合わせることで問題箇所の特定が捗った。&lt;/p&gt;
&lt;p&gt;ちなみに、contract test が参照すべき Open API 仕様はローカルファイルパスだけでなく URL 指定もできるので、provider&amp;ndash;consumer 関係の複数チームでも運用できそう。
本プロジェクトではひとまず monorepo でやるのでローカルパス指定でいく。&lt;/p&gt;
&lt;h4 id=&#34;dev-環境および-ci-におけるテスト設定のポイント&#34;&gt;Dev 環境および CI におけるテスト設定のポイント&lt;/h4&gt;
&lt;h5 id=&#34;dev-環境&#34;&gt;Dev 環境&lt;/h5&gt;
&lt;p&gt;開発中は検証サイクルを早くしたいので、image build を介さず、ホストの R ランタイムで直接起動した plumber server に対してテストするようにした:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# Makefile
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;run:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	VALIDATE_RESPONSE=true \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    LOG_LEVEL=$(LOG_LEVEL) \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    OPENAPI_SPEC_PATH=../../schema/openapi.yaml \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    PLUMBER_PORT=$(PLUMBER_DEV_PORT) \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Rscript run.R
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test-local:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	docker run --rm \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  -v $$(pwd)/../../schema:/schema \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  -v $$(pwd)/../../schemathesis.toml:/workspace/schemathesis.toml \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  -v $$(pwd)/../..:/workspace \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  -w /workspace \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  ghcr.io/schemathesis/schemathesis:stable \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  run /schema/openapi.yaml \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  --url http://host.docker.internal:$(PLUMBER_DEV_PORT) \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; 	  --coverage-format html --coverage-report-html-path ./schema-coverage.html
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本プロジェクトは現時点ではクラウド環境を作っておらず、compose で立ち上げるコンテナを暫定的に本番環境とみなしている。
このコンテナを使った動作確認も並行して行う機会があったので、dev 環境（ホストの R ランタイムで立ち上げた plumber サーバー）のポートは &lt;code&gt;PLUMBER_DEV_PORT&lt;/code&gt; として別に割り当てた。&lt;/p&gt;
&lt;p&gt;ちなみにコンテナ版の Schemathesis からホストのエンドポイントを叩くので、base URL には &lt;code&gt;host.docker.internal&lt;/code&gt; の利用が必須。&lt;/p&gt;
&lt;h4 id=&#34;ci&#34;&gt;CI&lt;/h4&gt;
&lt;p&gt;CI ではより本番環境に近い状態でテストしたいので、API サーバーはコンテナ版を使うようにした。
ただしデリバリー済みのイメージを使ってしまうと PR 時などの CI として意味がないので、ワークフロー内でイメージをビルドしている。&lt;/p&gt;
&lt;p&gt;CI で利用しているテストコマンドは下記:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# Makefile
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	docker run --rm \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  --network $(DOCKER_NETWORK) \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  -v $$(pwd)/../../schema:/schema \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; 	  -v $$(pwd)/../../schemathesis.toml:/workspace/schemathesis.toml \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  -v $$(pwd)/../..:/workspace \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  -w /workspace/.schemathesis \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  ghcr.io/schemathesis/schemathesis:stable \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  run /schema/openapi.yaml \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  --url http://monolith:$(CONTAINER_PORT) \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  --coverage-format html --coverage-report-html-path ./schema-coverage.html
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;dev 環境とパスがちょっと違うのは、レポート書き出し時に permission error になったのでディレクトリを掘って逃げた形跡。
あとは compose で立ち上げたサービスを叩くので docker network を使うようになっており、エンドポイントはサービス名 monolith で解決できる。&lt;/p&gt;
&lt;h3 id=&#34;リクエストレスポンスの検証&#34;&gt;リクエスト・レスポンスの検証&lt;/h3&gt;
&lt;p&gt;リクエストの検証に関しては、Schemathesis Fuzzy test のおかげで突貫ながらもそこそこ進められた。
&lt;code&gt;additionalProperties&lt;/code&gt; についてはやや迷ったのだが、今のところ BFF しか利用しない内部 API として作るので &lt;code&gt;false&lt;/code&gt; にしておいた。&lt;/p&gt;
&lt;p&gt;レスポンスの検証は dev 環境のみで有効になるようにした。
これはちょうど最近読んだ『データ指向プログラミング&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;』のプラクティスを参考にしている。&lt;/p&gt;
&lt;h3 id=&#34;api-ドキュメントの配信&#34;&gt;API ドキュメントの配信&lt;/h3&gt;
&lt;p&gt;せっかく API 仕様を管理していくので、API ドキュメントをどこかにホストしたい。
&lt;a href=&#34;https://scalar.com/&#34;&gt;Scalar&lt;/a&gt; というのがよさそうだったのでこれを使うことにした。
全然真面目に選定していないが、この手のツールは Open API document を読ませるだけなので、いざ問題が生じたとしても乗り換えコストは小さいはず。
ホスト先はいつも使っている Netlify にした。&lt;/p&gt;
&lt;p&gt;ドメインについては、下手に stock-assessment とかを当ててしまうと、方向転換を新規プロジェクトでやりたくなったときに不便なので、プロジェクト名からなるドメインにした: &lt;a href=&#34;https://api-docs.yoshimoto-samonji.rindrics.com/#tag/vpa&#34;&gt;https://api-docs.yoshimoto-samonji.rindrics.com/#tag/vpa&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;ハマったところ&#34;&gt;ハマったところ&lt;/h2&gt;
&lt;h3 id=&#34;sechemathesis-が生成した-query-method-によるテスト失敗&#34;&gt;Sechemathesis が生成した QUERY method によるテスト失敗&lt;/h3&gt;
&lt;p&gt;Schemathesis が（今年の 6 月に出たばかりの）&lt;a href=&#34;https://datatracker.ietf.org/doc/rfc10008/&#34;&gt;&lt;code&gt;QUERY&lt;/code&gt; method&lt;/a&gt; を投げてくれるのには困った。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;==================================== ERRORS ====================================
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_________________________________ POST /v0/vpa _________________________________
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Network Error
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Connection failed
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Connection aborted. Remote end closed connection without response
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Reproduce with:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    curl -X QUERY -H &amp;#39;Content-Type: application/json&amp;#39; -d &amp;#39;{&amp;#34;data&amp;#34;: {&amp;#34;caa_url&amp;#34;: &amp;#34;https://raw.githubusercontent.com/ichimomo/frasyr/dev/data-raw/ex2_caa.csv&amp;#34;, &amp;#34;waa_url&amp;#34;: &amp;#34;https://raw.githubusercontent.com/ichimomo/frasyr/dev/data-raw/ex2_waa.csv&amp;#34;, &amp;#34;maa_url&amp;#34;: &amp;#34;https://raw.githubusercontent.com/ichimomo/frasyr/dev/data-raw/ex2_maa.csv&amp;#34;}, &amp;#34;params&amp;#34;: {&amp;#34;m&amp;#34;: 0.5, &amp;#34;fc_year&amp;#34;: [2015, 2016, 2017], &amp;#34;tf_year&amp;#34;: [2015, 2016], &amp;#34;term_f&amp;#34;: &amp;#34;max&amp;#34;, &amp;#34;stat_tf&amp;#34;: &amp;#34;mean&amp;#34;, &amp;#34;pope&amp;#34;: true, &amp;#34;tune&amp;#34;: false, &amp;#34;p_init&amp;#34;: 0.5}}&amp;#39; http://host.docker.internal:31502/v0/vpa
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;確かに繋がらない:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-gdscript3&#34; data-lang=&#34;gdscript3&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;curl &lt;span style=&#34;color:#719e07&#34;&gt;-&lt;/span&gt;v &lt;span style=&#34;color:#719e07&#34;&gt;-&lt;/span&gt;X QUERY &lt;span style=&#34;color:#719e07&#34;&gt;-&lt;/span&gt;H &lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#39;Content-Type: application/json&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#719e07&#34;&gt;-&lt;/span&gt;d &lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#39;{&amp;#34;data&amp;#34;: {&amp;#34;caa_url&amp;#34;: &amp;#34;https://raw.githubusercontent.com/ichimomo/frasyr/dev/data-raw/ex2_caa.csv&amp;#34;, &amp;#34;waa_url&amp;#34;: &amp;#34;https://raw.githubusercontent.com/ichimomo/frasyr/dev/data-raw/ex2_waa.csv&amp;#34;, &amp;#34;maa_url&amp;#34;: &amp;#34;https://raw.githubusercontent.com/ichimomo/frasyr/dev/data-raw/ex2_maa.csv&amp;#34;}, &amp;#34;params&amp;#34;: {&amp;#34;m&amp;#34;: 0.5, &amp;#34;fc_year&amp;#34;: [2015, 2016, 2017], &amp;#34;tf_year&amp;#34;: [2015, 2016], &amp;#34;term_f&amp;#34;: &amp;#34;max&amp;#34;, &amp;#34;stat_tf&amp;#34;: &amp;#34;mean&amp;#34;, &amp;#34;pope&amp;#34;: true, &amp;#34;tune&amp;#34;: false, &amp;#34;p_init&amp;#34;: 0.5}}&amp;#39;&lt;/span&gt; http:&lt;span style=&#34;color:#719e07&#34;&gt;//&lt;/span&gt;localhost:&lt;span style=&#34;color:#2aa198&#34;&gt;31502&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;/&lt;/span&gt;v0&lt;span style=&#34;color:#719e07&#34;&gt;/&lt;/span&gt;vpa
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;*&lt;/span&gt; Host localhost:&lt;span style=&#34;color:#2aa198&#34;&gt;31502&lt;/span&gt; was resolved&lt;span style=&#34;color:#719e07&#34;&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;*&lt;/span&gt; IPv6: ::&lt;span style=&#34;color:#2aa198&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;*&lt;/span&gt; IPv4: &lt;span style=&#34;color:#2aa198&#34;&gt;127.0&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;0.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;*&lt;/span&gt;   Trying [::&lt;span style=&#34;color:#2aa198&#34;&gt;1&lt;/span&gt;]:&lt;span style=&#34;color:#2aa198&#34;&gt;31502.&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;..&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;*&lt;/span&gt; connect to ::&lt;span style=&#34;color:#2aa198&#34;&gt;1&lt;/span&gt; port &lt;span style=&#34;color:#2aa198&#34;&gt;31502&lt;/span&gt; from ::&lt;span style=&#34;color:#2aa198&#34;&gt;1&lt;/span&gt; port &lt;span style=&#34;color:#2aa198&#34;&gt;51462&lt;/span&gt; failed: Connection refused
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;*&lt;/span&gt;   Trying &lt;span style=&#34;color:#2aa198&#34;&gt;127.0&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;0.1&lt;/span&gt;:&lt;span style=&#34;color:#2aa198&#34;&gt;31502.&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;..&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;*&lt;/span&gt; Connected to localhost (&lt;span style=&#34;color:#2aa198&#34;&gt;127.0&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;0.1&lt;/span&gt;) port &lt;span style=&#34;color:#2aa198&#34;&gt;31502&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;&amp;gt;&lt;/span&gt; QUERY &lt;span style=&#34;color:#719e07&#34;&gt;/&lt;/span&gt;v0&lt;span style=&#34;color:#719e07&#34;&gt;/&lt;/span&gt;vpa HTTP&lt;span style=&#34;color:#719e07&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;1.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;&amp;gt;&lt;/span&gt; Host: localhost:&lt;span style=&#34;color:#2aa198&#34;&gt;31502&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;&amp;gt;&lt;/span&gt; User&lt;span style=&#34;color:#719e07&#34;&gt;-&lt;/span&gt;Agent: curl&lt;span style=&#34;color:#719e07&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;8.7&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;&amp;gt;&lt;/span&gt; Accept: &lt;span style=&#34;color:#719e07&#34;&gt;*/*&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;&amp;gt;&lt;/span&gt; Content&lt;span style=&#34;color:#719e07&#34;&gt;-&lt;/span&gt;Type: application&lt;span style=&#34;color:#719e07&#34;&gt;/&lt;/span&gt;json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;&amp;gt;&lt;/span&gt; Content&lt;span style=&#34;color:#719e07&#34;&gt;-&lt;/span&gt;Length: &lt;span style=&#34;color:#2aa198&#34;&gt;435&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;&amp;gt;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;*&lt;/span&gt; upload completely sent off: &lt;span style=&#34;color:#2aa198&#34;&gt;435&lt;/span&gt; bytes
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;*&lt;/span&gt; Empty reply from server
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;*&lt;/span&gt; Closing connection
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;curl: (&lt;span style=&#34;color:#2aa198&#34;&gt;52&lt;/span&gt;) Empty reply from server
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;どうしようもなかったので、いったんは &lt;a href=&#34;https://schemathesis.readthedocs.io/en/stable/reference/configuration/#phasescoverageunexpected-methods:~:text=parameters%20%3D%20true-,phases.coverage.unexpected%2Dmethods,-Type%3A%20Array&#34;&gt;&lt;code&gt;phases.coverage.unexpected-methods&lt;/code&gt;&lt;/a&gt; に空配列を指定して、仕様にないメソッドを使ったケース自体の生成を止めて対応した。
plumber 側にメソッドを追加する contrubution など、時間があったら検討するかも。&lt;/p&gt;
&lt;h3 id=&#34;dns-解決に時間のかかる-url&#34;&gt;DNS 解決に時間のかかる URL&lt;/h3&gt;
&lt;p&gt;現時点の実装では、データは URL を指定して ネットワーク越しに取得するようになっている&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;。
この理由から、例えば年齢別漁獲尾数をロードするために使われるパラメータ &lt;code&gt;caa_url&lt;/code&gt; は &lt;code&gt;format: uri&lt;/code&gt; になっているのだが、この情報をもとに Schemathesis は fuzzy test phase ではランダムな URL を生成してリクエストを投げてくる。
このランダムな URL の中に、まれに valid（ホストがいる）だが DNS 解決に時間がかかるものがあり（例えば &lt;code&gt;A.aG&lt;/code&gt;）、そのせいで契約テストが失敗することがあった。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ dig A.aG
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;; &amp;lt;&amp;lt;&amp;gt;&amp;gt; DiG 9.10.6 &amp;lt;&amp;lt;&amp;gt;&amp;gt; A.aG
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;; global options: +cmd
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;; Got answer:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;; -&amp;gt;&amp;gt;HEADER&amp;lt;&amp;lt;- opcode: QUERY, status: SERVFAIL, id: 30374
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;; OPT PSEUDOSECTION:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;; EDNS: version: 0, flags:; udp: 4096
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;; QUESTION SECTION:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;A.aG.				IN	A
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;; Query time: 3003 msec
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;; SERVER: 2404:1a8:7f01:a::3#53(2404:1a8:7f01:a::3)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;; WHEN: Mon Jul 13 03:16:29 JST 2026
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;;; MSG SIZE  rcvd: 33
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;テストを通すにはそのような URL の生成を抑制する方法も考えられたが、ドメインと無関係な知識（？）を持ち込むことになるのでやめた。
代わりに、DNS 解決に内部タイムアウトを設け、タイムアウトした場合には &lt;code&gt;Cannot resolve hostname&lt;/code&gt; として &lt;code&gt;404&lt;/code&gt; を返すようにした。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-R&#34; data-lang=&#34;R&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;check_dns &lt;span style=&#34;color:#719e07&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span style=&#34;color:#268bd2&#34;&gt;function&lt;/span&gt;(hostname) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  result &lt;span style=&#34;color:#719e07&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span style=&#34;color:#268bd2&#34;&gt;system&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#268bd2&#34;&gt;sprintf&lt;/span&gt;(&lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#34;timeout 2 nslookup %s &amp;gt;/dev/null 2&amp;gt;&amp;amp;1&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#268bd2&#34;&gt;shQuote&lt;/span&gt;(hostname)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    intern &lt;span style=&#34;color:#719e07&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#cb4b16&#34;&gt;FALSE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#586e75&#34;&gt;# timeout returns 124 if timed out, nslookup returns 1 if not found&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#268bd2&#34;&gt;if&lt;/span&gt; (result &lt;span style=&#34;color:#719e07&#34;&gt;%in%&lt;/span&gt; &lt;span style=&#34;color:#268bd2&#34;&gt;c&lt;/span&gt;(&lt;span style=&#34;color:#2aa198&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#2aa198&#34;&gt;124&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#268bd2&#34;&gt;stop&lt;/span&gt;(&lt;span style=&#34;color:#268bd2&#34;&gt;vpa_error&lt;/span&gt;(&lt;span style=&#34;color:#268bd2&#34;&gt;sprintf&lt;/span&gt;(&lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#34;Cannot resolve hostname: %s&amp;#34;&lt;/span&gt;, hostname), &lt;span style=&#34;color:#2aa198&#34;&gt;404&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;条件つき-nullability-のパラメータ&#34;&gt;条件つき nullability のパラメータ&lt;/h3&gt;
&lt;p&gt;frasyr の &lt;code&gt;vpa()&lt;/code&gt; の引数のうち、nullability が条件付きのものがある。
&lt;a href=&#34;https://ichimomo.github.io/frasyr/articles/vpa.html&#34;&gt;frasyr の利用法ドキュメント&lt;/a&gt;を見ると、例えば &lt;code&gt;abund&lt;/code&gt; などのチューニングに使う引数は &lt;code&gt;tune: true&lt;/code&gt; のときだけ必須となる。
このあたりを API 仕様にきちんと書かないと、fuzzy test がパターンを網羅的に生成するので &lt;code&gt;5XX&lt;/code&gt; 地獄になる。
意図しない組み合わせで呼ばれると、正しい使い方を促すエラーが frasyr からちゃんと出ているのだが、API としては仕様違反となって &lt;code&gt;500&lt;/code&gt; が返ってしまう。
直訳するならば JSON schema の &lt;a href=&#34;https://json-schema.org/understanding-json-schema/reference/conditionals&#34;&gt;&lt;code&gt;dependentRequired&lt;/code&gt;&lt;/a&gt; を使ってできそうではあるが、例えば optional なオブジェクト型の &lt;code&gt;withTune&lt;/code&gt; パラメータを定義したほうがシンプルかもしれない。
ひとまず、今回は時間がなかったので &lt;code&gt;tune&lt;/code&gt; が &lt;code&gt;false&lt;/code&gt; しか受け付けないものとして逃げた。
これはチューニング VPA をサポートする issue として個別に対応したほうがいいだろう。&lt;/p&gt;
&lt;h2 id=&#34;学んだこと&#34;&gt;学んだこと&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一つのエンドポイントが多くのパラメータを受け取ると、contract test が大規模になりがち&lt;/li&gt;
&lt;li&gt;Fuzzy test は強力。静的な生成物のよい使い方って感じ&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Contract test にやや手こずったが、依存の向きを逆転させて無事に API 契約駆動で開発できるようになったので今回はここまで。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&#34;https://rindrics.com/images/2026-07-12-api-contract/contract-ssot.webp&#34;
    alt=&#34;契約の生成（左）をやめて、API を契約に準拠させられるようになった（右）&#34;&gt;&lt;figcaption&gt;
      &lt;p&gt;契約の生成（左）をやめて、API を契約に準拠させられるようになった（右）&lt;/p&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&#34;参考情報&#34;&gt;参考情報&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://martinfowler.com/bliki/ContractTest.html&#34;&gt;Contract Testing | Martin Fowler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://spec.openapis.org/oas/v3.2.0&#34;&gt;OpenAPI Specification 3.2.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://scalar.com/&#34;&gt;Scalar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://schemathesis.readthedocs.io/en/stable/&#34;&gt;Schemathesis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://ichimomo.github.io/frasyr/articles/vpa.html&#34;&gt;VPA関数を使った資源量推定 | ichimomo/frasyr&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;&lt;a href=&#34;https://www.shoeisha.co.jp/book/detail/9784798179797&#34;&gt;Yehonathan Sharvit (2023) &amp;ldquo;データ指向プログラミング&amp;rdquo; 株式会社クイープ (訳), 翔泳社&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;将来的にデータ配信サービスを導入する可能性があるため&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description>
    </item>
    
    <item>
      <title>plumber を使ってみた</title>
      <link>https://rindrics.com/posts/stock-assess-plumber/</link>
      <pubDate>Tue, 16 Jun 2026 00:00:00 +0900</pubDate>
      
      <guid>https://rindrics.com/posts/stock-assess-plumber/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;../stock-assess-restart&#34;&gt;前回記事&lt;/a&gt;でリファクタリングの方針を立てたので、モノリスアプリケーションを作るところから始めてみる。&lt;/p&gt;
&lt;p&gt;とりあえず&lt;a href=&#34;https://github.com/Rindrics/yoshimoto-samonji&#34;&gt;リポジトリ&lt;/a&gt;を作った。
開発コードは &lt;a href=&#34;https://ja.wikipedia.org/wiki/%E7%BE%A9%E5%85%83%E5%B7%A6%E6%96%87%E5%AD%97&#34;&gt;yoshimoto-samonji&lt;/a&gt;（義元左文字）。&lt;/p&gt;
&lt;h2 id=&#34;技術選定&#34;&gt;技術選定&lt;/h2&gt;
&lt;p&gt;計算エンジンを Web UI から利用するモノリスアプリがゴールなら Shiny App 化すればいいのだが、
&lt;a href=&#34;../stock-assess-state-change&#34;&gt;評価ステータス管理の実験&lt;/a&gt;をしていた当時のように、最終的には評価「事業」全体をアプリ化したい。
丁寧なドメインモデリングが必要になるので、フロントは始めから型を扱える TypeScript で実装しておきたい。&lt;/p&gt;
&lt;p&gt;これを踏まえて、モノリスのバックエンドは &lt;a href=&#34;https://github.com/rstudio/plumber&#34;&gt;plumber&lt;/a&gt; を使って構築することにした。
&lt;a href=&#34;https://www.rplumber.io/&#34;&gt;ドキュメント&lt;/a&gt;をざっと見てみたが、OpenAPI ドキュメントを生成してくれるようなので、フロント&amp;ndash;バック境界は HTTP になる見込み。
ということは、フロントエンドに Next.js を使っておけば、Route handler を BFF に使える。
そうすると認証やセッション管理、画面向けのレスポンス整形を Next.js 側に寄せられるので、BFF を独立サービスとして立てる手間を抑えられそう。&lt;/p&gt;
&lt;p&gt;ここまでを &lt;a href=&#34;https://github.com/Rindrics/yoshimoto-samonji/commit/802416b2453da6f1a72ff54a4a8e13d0396a73aa&#34;&gt;ADR にした&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&#34;plumber-を使ってみる&#34;&gt;plumber を使ってみる&lt;/h2&gt;
&lt;h3 id=&#34;hello-plumber&#34;&gt;hello, plumber&lt;/h3&gt;
&lt;p&gt;始めて触るので、plumber を使った最低限の実装を書いてみる。&lt;/p&gt;
&lt;p&gt;とりあえず&lt;a href=&#34;https://github.com/Rindrics/yoshimoto-samonji/commit/f854c38dd48a75bdabf3aaa51f387c3a513edada&#34;&gt;パッケージをセットアップ&lt;/a&gt;し、
&lt;a href=&#34;https://www.rplumber.io/reference/index.html&#34;&gt;reference&lt;/a&gt;を&lt;a href=&#34;https://github.com/Rindrics/yoshimoto-samonji/commit/93bd9d1f70e2e300c00cffc1bc0b2aac34a0b2a7&#34;&gt;参考に実装してみた&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Plumber&lt;/code&gt; オブジェクトの &lt;code&gt;$run()&lt;/code&gt; を実行する関数を作り、&lt;code&gt;devtools::load_all()&lt;/code&gt; してから呼び出すと、指定したポートでサーバーが起動した:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-gdscript3&#34; data-lang=&#34;gdscript3&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;&amp;gt;&lt;/span&gt; devtools::load_all(); start_server()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ℹ Loading legacy
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Running plumber API at http:&lt;span style=&#34;color:#719e07&#34;&gt;//&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;127.0&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;0.1&lt;/span&gt;:&lt;span style=&#34;color:#2aa198&#34;&gt;31501&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Running swagger Docs at http:&lt;span style=&#34;color:#719e07&#34;&gt;//&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;127.0&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;0.1&lt;/span&gt;:&lt;span style=&#34;color:#2aa198&#34;&gt;31501&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;/&lt;/span&gt;__docs__&lt;span style=&#34;color:#719e07&#34;&gt;/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;クエリパラメータ &lt;code&gt;msg&lt;/code&gt; に渡した文字列を利用したレスポンスを返す API ができた:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ curl &lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#34;http://127.0.0.1:31501/vpa/echo?msg=hello&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#34;msg&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#719e07&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#34;The message is: &amp;#39;hello&amp;#39;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#719e07&#34;&gt;]}&lt;/span&gt;%
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;また、&lt;code&gt;/__docs__&lt;/code&gt; にアクセスすることで Swagger UI も利用できた:
&lt;figure&gt;&lt;img src=&#34;https://rindrics.com/images/2026-06-16-plumber/swagger.webp&#34;
    alt=&#34;生成された API ドキュメント。UI から動作確認できる&#34;&gt;&lt;figcaption&gt;
      &lt;p&gt;生成された API ドキュメント。UI から動作確認できる&lt;/p&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;せっかく OpenAPI を利用できているので、将来的にはクライアントコードを生成するなどして契約駆動開発していきたい。
API スキーマをファイルに直接書き出すメソッドは無さそうだったが、&lt;a href=&#34;https://www.rplumber.io/reference/Plumber.html?q=getApiSpe#method-getapispec-&#34;&gt;&lt;code&gt;getApiSpec()&lt;/code&gt;&lt;/a&gt; を使うと仕様をリストで取得できそうだったので、これを &lt;a href=&#34;https://github.com/Rindrics/yoshimoto-samonji/commit/2bb09a9ece31f99d20ef471acfa754c8f4518479&#34;&gt;JSON にマッピングして書き出すことにした&lt;/a&gt;。
スキーマはいったん ignore しておいて、クライアントコード生成などは Next.js のセットアップをしてから整えることにした。&lt;/p&gt;
&lt;p&gt;ちなみに生成されたスキーマを見ると、設定した覚えのないメタデータが既に入っていた:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#268bd2&#34;&gt;&amp;#34;openapi&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#34;3.0.3&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#268bd2&#34;&gt;&amp;#34;info&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#268bd2&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#34;API Description&amp;#34;&lt;/span&gt;], &amp;lt;- このへんは設定した覚えがない
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#268bd2&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#34;API Title&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#268bd2&#34;&gt;&amp;#34;version&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#2aa198&#34;&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href=&#34;https://www.rplumber.io/articles/annotations.html?q=description#annotations-example&#34;&gt;ドキュメント&lt;/a&gt;を見ると設定方法がちゃんと書いてあって、アノテーションか &lt;code&gt;pr_set_api_spec()&lt;/code&gt; を使って設定できるらしい。
初めドキュメントの通りにアノテーションを足したつもりが、スキーマに反映されなかったが、&lt;a href=&#34;https://github.com/Rindrics/yoshimoto-samonji/commit/091c2d5254d4a7fa7ccb13d25ecb8882508e3d42#diff-5714ab9b3a287e926760b71b0596e65ca649a97e2552d861726619fef0a91ffaL2&#34;&gt;このように&lt;/a&gt; &lt;code&gt;Plumber&lt;/code&gt; オブジェクトを作っているファイル名を &lt;code&gt;pr()&lt;/code&gt; の &lt;code&gt;file&lt;/code&gt; 引数に指定することで反映されるようになった。
ドキュメントでは引数なしで動いているように見えたが、断りがない限り &lt;code&gt;Plumber&lt;/code&gt; を作るファイル名は &lt;code&gt;plumber.R&lt;/code&gt; になっている想定らしい。&lt;/p&gt;
&lt;h3 id=&#34;コンテナ-image-化&#34;&gt;コンテナ image 化&lt;/h3&gt;
&lt;p&gt;R ではライブラリ系の使われ方をする開発しかしたことなかったので、思考停止でパッケージアプローチを取る癖がついてしまっていたけど、
今回コンテナ用イメージを作る段になって、ランタイム系のイメージを作るにはパッケージアプローチは無駄が多すぎることに気づいた。
&lt;code&gt;devtools&lt;/code&gt; のような開発系ツールや、その他パッケージ化に必要なメタデータもランタイムとしては完全に無駄なので、結局シンプルなスクリプト構成に移行した。&lt;/p&gt;
&lt;p&gt;plumber がどうやって動作しているかを理解していなかったせいで、スクリプト形式に移行する過程でだいぶハマった。
それはそれで学ぶこともあったので、時間があったら別記事にする。&lt;/p&gt;
&lt;h2 id=&#34;vpa-を実行する-image-を作る&#34;&gt;VPA を実行する image を作る&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://ichimomo.github.io/frasyr/articles/vpa.html&#34;&gt;こちらのドキュメント&lt;/a&gt;を参考に
とりあえず &lt;code&gt;vpa()&lt;/code&gt; を実行することにした。
データの保存場所は恐らく分かれると思うので、それぞれのデータは URL で受け取るようにした。
リクエストもレスポンスも、設計余地がけっこうありそうなので、まだ仮の実装にしてある。
将来的に API 仕様を変えることが明らかなので、予めバージョンを切っておいた。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&#34;https://rindrics.com/images/2026-06-16-plumber/post-vpa.webp&#34;
    alt=&#34;バージョンを切った。&#34;&gt;&lt;figcaption&gt;
      &lt;p&gt;バージョンを切った。&lt;/p&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;まず一つ API を作ってみた感じでは、見積もりの 5 倍くらいは時間がかかった気がする。
ただし、内訳としては plumber の使い方自体だったり、ビルドプロセスの調整などに手間取っていた。
API 自体の設計とかは全然していなかったので、今後は別のところに時間がかかるようになるはず。&lt;/p&gt;
&lt;h2 id=&#34;学んだこと&#34;&gt;学んだこと&lt;/h2&gt;
&lt;p&gt;改めて、使われ方に応じた構成をとることが大事。
型がないとスキーマ定義などがめちゃめちゃ面倒。&lt;/p&gt;</description>
    </item>
    
  </channel>
</rss>
