共通基盤を1か所へ — 5つの共有リポジトリを1つに統合した

上原正吉(EarthLink Network Co., Ltd.)。Claude Codeを開発の主体に据え、20を超えるプロダクトを1人で同時に開発・運用しています。これは、その現場の実測記です。

共通基盤を1か所へ — 5つの共有リポジトリを1つに統合した

共通基盤を1か所へ — 5つの共有リポジトリを1つに統合した

2026 年 6 月、私の手元には共通ロジックを抱えたリポジトリが 5 つ、バラバラに存在していました。ユーティリティ集、DynamoDB のヘルパー、レートリミッタ、ブロックエディタ機構、そして通知。それぞれ別のタイミングで生まれ、別のプロダクトから参照され、依存の向きも publish 先も揃っていない。この記事では、それを 1 つの monorepo に集約し、「共通基盤(provider)とプロダクト(consumer)」の境界をどう引き直したか、そこで一番効いた設計判断は何だったかを書きます。

先に結論を置きます(本文の読了は約 7 分)。

  • 散らばった 5 リポジトリを 1 か所へ集約する — ユーティリティ・DynamoDB 共通・共有ライブラリ・ブロックエディタ・通知を、1 つの monorepo(provider)にまとめました。
  • 共通基盤は runtime を持たない — API・画面・データの実体は consumer 側のプロダクトに残し、provider は使い回すコードだけを持ちます。
  • 共通パッケージは process.env を読まない — テーブル名などの環境依存の値は consumer から注入させます。これが一番効いた判断で、パッケージがどの環境にも移植できます。
  • 集約が割に合うのは consumer が 2 つ以上になってから — v1.0.0 移行では片方で約 60 ファイルが変わり、もう片方ではローカルに抱えていたエディタ実装 15 ファイルを削除できました。共通化で現れるのは新しい共通コードではなく、各プロダクトが個別に抱えていた重複コピーが消えることです。1 つの consumer のためだけなら集約の手間は見合いませんが、2 つ目が同じコードを必要とした時点で割に合い始めます。

散らばっていた 5 つの共有リポジトリ

集約前の状態を棚卸しすると、こうなっていました。

  • ユーティリティ集(2024 年 8 月、最古): 型安全な書き方を助ける小さな部品(成功/失敗を型で扱う Result・Option、条件分岐を網羅的に書く ts-pattern、入力を検証する zod、構造化ロギングなど)を束ねた公開 npm ツールキット。
  • DynamoDB 共通(2026 年 1 月、v1.0.2): GitHub Packages に publish 済みの DynamoDB ヘルパー。
  • 共有ライブラリ(2026 年 5 月): 一定時間あたりのリクエスト数を制限するレートリミッタ(上限を超えた呼び出しを待たせる・弾く仕組み)などをまとめたものです。
  • ブロックエディタ(2026 年 6 月): block-cmsblock-editor などのパッケージ群。2 つのプロダクトが同時に参照する「唯一のソース」。
  • 通知: もともと別サービスとして生まれ、あとから共通基盤へ引っ越してきた。

問題は「共通ロジックが 5 か所に散り、各プロダクトがそれぞれ違うやり方で参照している」ことでした。あるものは公開 npm、あるものは GitHub Packages、あるものはローカルにコピー。更新のたびに「どれが正か」を探す羽目になります。

選択肢 A / B / C と、選んだ形

整理にあたって 3 つの道を考えました。

  • A: 各リポジトリを独立のまま個別に publish し続ける — 既存の形に近く、移行コストは低い。ただし 5 リポジトリ分の CI・バージョニング・依存グラフを、この先ずっと別々に面倒見ることになります。
  • B: 1 つの monorepo に集約し、@org/* として GitHub Packages に publish する — CI とバージョニングを 1 か所に寄せられます。
  • C: 何もせず、ローカルコピーで重複させ続ける — 論外です。実際にプロダクト側へブロックエディタのローカルコピーが溜まり、二重管理になりかけていました。

選んだのは B です。6 月 25 日に共通パッケージ monorepo を scaffold し、packages/ 配下にユーティリティ・DynamoDB 共通・共有ライブラリ・ブロックエディタ・通知を集めました。README に置いた一文が、この基盤の性格を決めています。

ランタイム(API / 管理画面 / データ)は持たない。それらはプロダクト側に同居させる。

つまりこの monorepo は provider(ライブラリの供給者)に徹し、runtime を持たない。API も画面もデータアクセスの実体も、consumer 側のプロダクトが持つ。集約するのは「使い回すコードだけ」と線を引いたわけです。

1つに集約した共通パッケージ monorepo の packages/ 構成(画面は再構成・匿名、構成は実測)。ユーティリティ・DynamoDB 共通・共有ライブラリ・ブロックエディタ・通知が同居し、API や画面といった runtime は持ちません

一番効いた判断: パッケージは process.env を読まない

集約そのものより効いた判断は、共通基盤(provider)と利用側(consumer)の境界をどこに引くかでした。ブロックエディタの README にその原則が明文化されています。

| @org/block-cms | ブロック CMS(型・repository・dynamodb-client・
  seeds・theme-presets。テーブル名は consumer から注入。
  パッケージ内で process.env を読まない) |

共通パッケージの中で process.env を読まない。DynamoDB のテーブル名のような環境依存の値は、パッケージが自分で環境変数から拾うのではなく、consumer(プロダクト)から引数・設定オブジェクトとして注入させる。これが、この設計の中心です。

なぜこれが重要か。もし共通パッケージが process.env.DYNAMODB_TABLE を内部で読んでしまうと、そのパッケージは「特定の環境変数名」という暗黙の契約を consumer に強制します。プロダクト A は DYNAMODB_TABLE、プロダクト B は TABLE_NAME を使っていたら、もう共通化できません。さらに、Amplify SSR のように実行時に env が伝搬しづらい環境もあり、「ライブラリが env をいつ読むか」が consumer 側のデプロイ形態に依存してしまう。env を読む責務を consumer に一元化することで、共通パッケージは純粋な関数群でいられ、どの環境にも移植できる。テストも、env をモックせず引数を渡すだけで書けます。

consumer 側は GitHub Packages を向く .npmrc を置いて取得します。

# consumer 側 .npmrc
@org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}

そして provider 側のビルド・publish は monorepo にまとまるので、pnpm -r build で全パッケージを一括ビルドできます。React UI も純ロジックも同居しますが、pnpm workspaces がパッケージごとに依存と tsconfig を分離するので、UI パッケージの依存が純ロジック側に漏れることはありません。

一番効いた判断。共通パッケージは自分で process.env を読まず、テーブル名などの環境依存値は consumer から注入させます(画面は再構成・匿名、原則は README の実測)。左が provider の原則、右が consumer 側の .npmrc と注入の形です

移行運用でわかったこと

集約の山場は、プロダクト側をローカル実装から共通パッケージ参照へ切り替える移行でした。6 月上旬、2 つのプロダクトの LP 機能を共通パッケージ v1.0.0 に移行したフェーズでは、片方で約 60 ファイルが変更になり、もう片方ではローカルに抱えていたエディタ実装 15 ファイルを削除しました。「共通化」は足し算ではなく、consumer 側の重複コードを消す引き算として現れます。この引き算が効いた瞬間に、初めて「1 か所」の恩恵が出ます。

共通パッケージ v1.0.0 への移行 PR の変更行(画面は再構成・匿名、数値は実測)。片方のプロダクトで約 60 ファイルが変わり、もう片方ではローカルに抱えていたエディタ実装 15 ファイルを削除。共通化の効果は、コードが増えるのではなく、重複が消えて減ることで表れます

もう 1 つ、通知サービスの引っ越しは「責務の移管」でした。もともと独立サービスだったものを共通基盤の packages/ に取り込み、送信ロジックとクライアントを分けて(notify / notify-client)、プロダクトからは薄いクライアント越しに使う形にしました。provider に寄せるとき、何を provider に置き何を consumer に残すかは、通知の場合「送る仕組みは共通、送る内容や宛先はプロダクト固有」で切りました。

転用できる条件

この集約が効くのは「複数の consumer が同じロジックを参照する」ときだけです。consumer が 1 つしかないなら、monorepo 集約のオーバーヘッド(publish・バージョニング・CI)は割に合わず、プロダクトに同居させたほうが速い。逆に 2 つ以上が同じコードのローカルコピーを持ち始めたら、それが集約のサインです。私の場合、ブロックエディタを 2 プロダクトが参照し始めたのが転換点でした。

転用できる教訓

  • 共通基盤は「使い回すコードだけ」を持ち、runtime(API・画面・データの実体)は持たせない。provider と consumer の役割を最初に分ける。
  • 共通パッケージの中で process.env を読まない。環境依存の値は consumer から注入させる。これで env を読む責務が一元化され、パッケージがどの環境にも移植できる。
  • 「共通化」の成果は足し算ではなく、consumer 側の重複コードが消える引き算として現れる。移行 PR で削除行が増えたら成功のサイン。
  • 集約するのは consumer が 2 つ以上になってから。1 consumer なら同居のほうが速い。ローカルコピーが 2 つ目にできた瞬間が転換点。

筆者について

上原正吉。EarthLink Network Co., Ltd. でAI開発をしています。2025年からClaude Codeを開発の主体に据え、今は20を超えるプロダクトを1人で同時に開発・運用しています。この連載では、その現場で実際に起きたこと(うまくいったことも、失敗も)を、数字と一緒に書いていきます。

また、AIで業務や開発を組み替えたい会社・チーム向けに、AI活用のコンサルティングも受け付けています。ご相談は www.eln.ne.jp からどうぞ。


EarthLink Network は、会社の全業務を AI で回すために、必要になったものを自社で作っています。いま作っているプロダクトの一覧と概要は、こちらにまとめています。

EarthLink Network が自社でつくっている18のプロダクト

会社と各プロダクトの詳細は、公式サイト www.eln.ne.jp をご覧ください。

共通基盤を1か所へ — 5つの共有リポジトリを1つに統合した