シリーズ最終回。jpzip の SDK 8 言語版(Go / TypeScript / Python / Rust / Ruby / Dart / PHP / Swift)を、Claude Code を相棒に 6 時間で書いた話です。先に1 本目・2 本目・3 本目を読むとこの記事の数字が腑に落ちます。
#TL;DR
- AI 駆動開発(Claude Code)で、8 言語の SDK を 6 時間で実装しました
- 公開先: Go / npm / PyPI / crates.io / RubyGems / pub.dev / Packagist / Swift Package Index
- すべての SDK が 同じ API シグネチャ・同じキャッシュ設計・同じリトライ設計を持つ
- 鍵は 「先にプロトコルを文章で固める」こと。実装は AI に任せられる粒度まで落とす
- 言語固有の落とし穴は確実にあるので、「言語ごとの慣習に翻訳してもらう」プロンプトに切り替える
#「8 言語 6 時間」の正体
最初に正直に書いておきます。「6 時間で 8 言語の SDK を書いた」という言い方は、Go SDK の開発開始から 8 言語ぶんの初回 publish までの時間を指しています。タイマーをスタートさせる前に、こうした準備が積み上がっていました:
- データセット(120,677 件の JSON)が CDN に公開済み(第 2 回参照)
- プロトコル仕様
spec/v1/protocol.mdが JSON Schema 込みで固定済み
つまり「仕様と CDN が揃った状態から、Go を参照実装として 8 言語ぶんを一気に書いて publish した」のが 6 時間です。これは AI 駆動開発でできた、というより、AI 駆動開発のためにできる前提を全部揃えてから始めた結果です。
#全体の流れ
ざっくりこういう順番でした。
- プロトコルを文書で固める(事前)
- Go SDK を 1 つだけきっちり書く(6 時間の中の前半)
- Go SDK を参照実装として、残り 7 言語に展開(6 時間の中の後半)
2 と 3 を合わせて 6 時間。3 つ目のフェーズは、ほぼ「Claude Code に翻訳してもらう」作業でした。
#仕様の固め方
8 言語の SDK が同じ挙動になる条件は、結局「全員が同じ仕様を読んでいる」ことです。これを文書として完成させる:
spec/
├── README.md
├── CHANGELOG.md
├── LICENSE
├── schema/
│ └── v1/
│ ├── zipcode-entry.json # JSON Schema
│ └── meta.json # JSON Schema
└── spec/
└── v1/
└── protocol.md # プロトコル本文
protocol.md には:
- エンドポイント一覧と各レスポンス例
- JSON Schema(型は機械可読、説明は人間可読)
- CORS / Cache-Control の規約
- バージョニング規約(マイナーは後方互換)
…が書いてあります。これを Claude に読ませてから「Go で実装して」と頼める ところまで持っていくのが目標でした。実際、最初の Go SDK はほぼこの 1 文で書き始めています。
#各 SDK に共通させた API
lookup / lookupGroup / lookupAll / preload / getMeta の 5 関数だけ。これを言語の慣習で命名し直しました。
| 言語 | パッケージ | 単発検索 |
|---|---|---|
| Go | github.com/jpzip/go | jpzip.Lookup(ctx, "2310017") |
| TypeScript | @jpzip/jpzip | await lookup("2310017") |
| Python | jpzip | lookup("2310017") / await client.lookup(...) |
| Rust | jpzip | jpzip::lookup("2310017").await? |
| Ruby | jpzip | Jpzip.lookup("2310017") |
| Dart | jpzip | await lookup("2310017") |
| PHP | jpzip/jpzip | lookup("2310017") |
| Swift | Jpzip | try await lookup("2310017") |
「動詞 + 名詞」の構造を変えない、戻り値の null 表現を言語固有のものに合わせる(null / nil / Option / ?ZipcodeEntry)、これだけは厳守してもらいました。
#共通の挙動
API シグネチャだけでなく、内部挙動も全 SDK 共通です:
- HTTP リトライ: 5xx / ネットワークエラーで 3 回まで指数バックオフ
- L1 キャッシュ: メモリ上の LRU(プレフィックスファイル単位 + エントリ単位)
- L2 キャッシュ: 任意の永続キャッシュ(インタフェースを切ってあるので、ファイル / Redis / SQLite 何でも差し込める)
- L3 キャッシュ: HTTP の
Cache-Controlを尊重(Pages 側で 24h TTL) - lookupAll:
/g/0..9.jsonを並列 fetch して in-memory dict にマージ
L2 キャッシュは「interface を切る」だけで、デフォルト実装は提供していません。各言語で実装は数十行。
#6 時間の中身
最初の Go SDK を書いた時点で参照実装が手に入ります。残り 7 言語は、Claude Code への指示を 「翻訳タスク」として組み立てるのが効きました。
#プロンプトの基本形
ここに Go の参照実装がある。
これを Ruby の慣習に従って書き直してほしい。
絶対に守ること:
- 公開 API は { Jpzip.lookup, Jpzip.lookup_group, ... } の名前
- 戻り値は frozen Data オブジェクト(Ruby 3.2+ の Data.define)
- HTTP は net/http のみ(外部 gem 禁止)
- リトライは 3 回・指数バックオフ
- L2 キャッシュは Module で interface を切ってあり、デフォルト実装は無し
避けること:
- Active Support 系の拡張
- スレッドセーフでない実装(Monitor を使うこと)
- メソッド名のキャメルケース化
「Go の API を そのまま移植」だと変なコードが上がってきます。逆に「Ruby の慣習に従って 翻訳」と頼むと、L1 LRU が Hash ベースに置き換わったり、retry が rescue retry の構造になったり、ちゃんと言語のスタイルになる。
#「動かしてから直す」を 1 言語ずつ
各言語で:
- テストスイートを Go から翻訳してもらう(fixture と期待値は同じ)
- 実装を翻訳してもらう
- テスト実行
- 落ちた箇所を Claude に投げて直してもらう
これを 7 言語ぶんやりました(Go は参照実装側なのでこのループには入りません)。実装より 「言語固有の落とし穴を Claude に教える」プロンプト に時間を使った印象です。
#出てきた言語別の癖
実装中に「ああ、この言語こうなんだ」と気づいた点をいくつか:
- Rust:
openssl-sysを避けてrustlsを強制した(C toolchain なしで build できる方が SDK は嬉しい) - Python: 同じ実装を sync (
httpx.Client) と async (httpx.AsyncClient) で書き分けるのではなく、インタフェースを共有させて 2 つのバックエンドが差し込まれる形に整理 - Ruby: スレッドセーフ性を
Monitorで素直に書く(Mutexより文脈に合う) - Dart: Flutter / CLI / Server / Flutter Web の 4 ターゲットで同じコードが動くように、
dart:ioではなくpackage:http経由のみ - PHP: 8.2+ の
readonly classで値オブジェクトを表現、HTTP は Guzzle 7 - Swift:
async/awaitを素直に使う(コールバック地獄を避ける)
これらは AI に「Rust だけど C toolchain なしで作って」「Dart は Flutter Web でも動かないとダメ」と 制約を 1 行ずつ追加していくと、AI が勝手にこういう設計に落としてくれます。
#どこが効いて、どこが効かなかったか
#効いたこと
- プロトコル先行: 全 SDK の挙動を文章で先に決めた。これが Claude に渡せる「正解の定義」になった
- Go を最初に書く: Go は型と error が明示的で、参照実装として「曖昧さが残らない」言語。Python や Ruby を参照にすると暗黙の挙動が翻訳側に漏れる
- テスト先行翻訳: テストを最初に翻訳すると、実装の正解判定が自動化される
- 言語の慣習をプロンプトで指定: 「Pythonic に」「Idiomatic Rust で」と書くだけで品質が変わる
- CI を GitHub Actions で揃える: 8 言語ぶんの publish ワークフローを Claude にコピペで作らせた
#効かなかったこと
- 「全部一気にやって」と頼む: コンテキストが膨らみすぎてミスが増える。1 言語ずつ完結させるほうが結局速い
- テストなしで実装だけ翻訳: そこそこ動くけど微妙な挙動差が残る。テスト翻訳とセットで頼むべき
- エラーメッセージを言語横断で揃えようとする: 言語固有の例外哲学があるので無理に統一しない方がよかった
#「8 言語 SDK」は誰のためのものか
「そんなに使われる言語あるの?」と聞かれそうですが、これは 「Claude が 8 言語のうちどれを選んでも使える」 ことに本当の意味があると思っています。
普段 TypeScript を書いている開発者の Rust プロトタイプで、@jpzip/jpzip ではなく jpzip クレートが使える。これは「言語選択の自由」を保つうえで地味に効きます。実際、SDK 公開後にいただいた反応は「ちょうど書いてる言語にあって助かった」が多かったです。
#AI 駆動開発の何が変わるのか
8 言語 SDK プロジェクトを通じて変わった肌感覚:
- 「実装するモノ」と「生成するモノ」の境界線が動いた
- プロトコルを文章にすれば、SDK は「書く対象」から「生成する対象」になる
- 設計の比重が前倒しになる
- 仕様 / 参照実装 / テストを先に作る時間が増える代わりに、実装ループが短くなる
- 「言語の壁」が薄くなる
- 「自分が書いたことのない言語の SDK」を、その言語の慣習に従って出せる
- 品質の天井は AI ではなく、プロトコルの完成度で決まる
- 仕様がブレていれば 8 言語ぶんブレる。仕様が綺麗なら 8 言語ぶん綺麗に出る
#このシリーズで書いた 4 本
- Cloudflare Pages 無料枠だけで micro-SaaS データセットを作った話
- KEN_ALL.csv を Cloudflare Pages から 120,677 件配信する設計
- MCP サーバーを書いて Claude が郵便番号を扱えるようにした
- 本記事: Claude Code 1 人開発で 6 時間で 8 言語 SDK を実装した話
このシリーズを通して見えたのは、「個人開発の射程は、AI と組むことで一段広がっている」ということでした。データ層・プロトコル層・クライアント層の分離設計があると、AI 駆動開発と相性がいい。逆にいえば、AI 駆動開発を前提に置くと、設計の優先順位も少し変わります。
#使ってみてください
| 言語 | インストール |
|---|---|
| Go | go get github.com/jpzip/go |
| TypeScript | npm i @jpzip/jpzip |
| Python | pip install jpzip |
| Rust | cargo add jpzip |
| Ruby | gem install jpzip |
| Dart | dart pub add jpzip |
| PHP | composer require jpzip/jpzip |
| Swift | Swift Package Manager で Jpzip を追加 |
GitHub: https://github.com/jpzip サイト: https://jpzip.nadai.dev/
普段使っている言語があれば、3 行で動きます。AI 駆動でこういう「複数言語にまたがる小さなライブラリ群」を作るのは、想像以上に楽しい体験でした。