ハードコーディングされた `vite.config.ts` はなぜ悪なのか?:開発環境のAPIプロキシを「完全自動検知」させるアーキテクチャ設計
開発環境において、フロントエンドのビルドツール(Vite / Webpack)が提供する開発サーバーのプロキシ機能は、CORSの呪縛から開発者を解放する不可欠なライフラインだ。
しかし、多くの現場で未だに次のようなアンチパターンが放置されている。
// 多くのプロジェクトで見かける、技術的負債に満ちたハードコーディング
export default defineConfig({
server: {
proxy: {
‘/api’: ‘http://localhost:8080’, // 「俺のPCでは8080だが、隣のあいつは3000だ」という地獄の始まり
}
}
})
この静的な設定は、マイクロサービスアーキテクチャ、Dockerコンテナ化された開発環境、CI/CDでのE2Eテスト、そして開発者ごとのローカル環境の差異(ポート枯渇やポートフォワードの競合)の前に、容易に破綻する。
本稿では、Viteの内部プロキシ機構(Connectベースのミドルウェア)の挙動を解剖し、環境変数、Dockerのネットワークトポロジー、さらにはローカルの動的ポートスキャンやロックファイルを自動検知して「プロキシ先を100%自動でルーティングする自律型 `vite.config.ts`」を構築する極限の自動化手法を提示する。
—
1. Viteプロキシの内部アーキテクチャ:なぜ動的制御が必要なのか
Viteの開発サーバーは、その下層で Connect(Node.js向けのミニマルなWebフレームワーク)を駆動させている。`server.proxy` オプションに渡されたオブジェクトは、内部で `http-proxy` ライブラリのインスタンスへと変換される。
[Browser]
│
├─ GET /api/users
│
▼
[Vite Dev Server (Connect Middleware)]
│
├─ Matches `/api` ?
│ Yes ──► [http-proxy] ──► [Dynamic Target Resolver] ──► [Backend API Server]
│ No ──► Serve static / Transform ESM
静的な文字列や単一のURLオブジェクトを渡した場合、Viteの起動プロセス(`vite dev`)が走った瞬間にプロキシの宛先が固定化される。つまり、次のような動的要件に対応できない。
1. マイクロサービスのマルチバックエンド化: `/api/auth` はポート 4001 へ、`/api/orders` はポート 4002 へ、残りは 8080 へ振り分けたい。
2. Docker環境とホストマシンの共存: ホストで動かす場合と、DevContainer等のコンテナ内で動かす場合で、ループバックアドレス(`localhost` vs `host.docker.internal` vs Docker Bridge IP)を動的に切り替えたい。
3. 動的ポートアロケーション: バックエンド側がランダムポートやプロセスフォワードで起動している場合、起動時にそのメタデータ(`.env.local` や共有ロックファイル)をVite側が自動検知しなければならない。
これらを解決するには、`vite.config.ts` を単なる「設定ファイル」ではなく、「環境トポロジーを動的に解決するオーケストレーション・スクリプト」として昇華させる必要がある。
—
2. 実装:自律型ダイナミック・プロキシ・リゾルバの構築
ここから、実際のプロダクション環境で即座に採用できる高度な `vite.config.ts` の実装コードを解説する。
この実装では以下の要件を満たす。
- 複数のバックエンドサービス(API, Auth, Webhook)のプレフィックスを動的にルーティング。
- 環境変数、またはローカルのオーケストレーションファイル(Docker Composeの出力やポートマップJSON)から自動的に接続先を検出。
- 接続先が死んでいる(起動していない)場合に、開発者へ明確なフォールバックと警告をログ出力。
`vite.config.ts` の完全実装
import { defineConfig, loadEnv, UserConfig } from ‘vite’
import react from ‘@vitejs/plugin-react’
import fs from ‘node:fs’
import path from ‘node:path’
/
- 1. バックエンドサービスのルーティング定義型
/
interface ServiceRoute {
prefix: string;
defaultPort: number;
envKey: string;
}
const SERVICES: ServiceRoute[] = [
{ prefix: ‘/api/auth’, defaultPort: 4001, envKey: ‘VITE_AUTH_PORT’ },
{ prefix: ‘/api/orders’, defaultPort: 4002, envKey: ‘VITE_ORDERS_PORT’ },
{ prefix: ‘/api’, defaultPort: 8080, envKey: ‘VITE_API_PORT’ }, // フォールバックは最後
];
/
- 2. Docker環境およびホスト環境の差異を吸収するホスト解決ロジック
/
function resolveHostTarget(): string {
// Dockerコンテナ内からの接続か判定(/.dockerenvの存在確認)
const isInsideDocker = fs.existsSync(‘/.dockerenv’);
if (isInsideDocker) {
// Docker内部からホスト側サービスを叩く場合の標準IP
return process.env.DOCKER_HOST_IP || ‘host.docker.internal’;
}
return ‘localhost’;
}
/
- 3. 動的ポート解決の仕組み
- ローカルで複数のプロセスが立ち上がっている場合、コンテナ間連携ファイルや
- 独自の一時JSONファイル(例: .port-lock.json)から最新のポート番号を動的取得する。
/
function getTargetPort(service: ServiceRoute, mode: string, env: Record
// A. 環境変数からの明示的な指定を最優先
if (env[service.envKey]) {
const parsed = parseInt(env[service.envKey], 10);
if (!isNaN(parsed)) return parsed;
}
// B. Docker Composeやローカルオーケストレータが吐き出すポートマップファイルを探索
const lockFilePath = path.resolve(process.cwd(), ‘.dev-ports.json’);
if (fs.existsSync(lockFilePath)) {
try {
const portMap = JSON.parse(fs.readFileSync(lockFilePath, ‘utf-8’));
if (portMap[service.envKey]) {
return Number(portMap[service.envKey]);
}
} catch (e) {
console.warn(`[Vite Proxy] Failed to parse .dev-ports.json:`, e);
}
}
// C. すべて該当しない場合はデフォルトポートへフォールバック
return service.defaultPort;
}
export default defineConfig(({ mode }) => {
// 指定されたモード(development等)の環境変数をロード
const env = loadEnv(mode, process.cwd(), ”);
const host = resolveHostTarget();
// プロキシ設定オブジェクトを動的に構築
const proxyConfig: Record
SERVICES.forEach((service) => {
const port = getTargetPort(service, mode, env);
const targetUrl = `http://${host}:${port}`;
console.log(`[Vite Proxy Engine] Route mapped: ${service.prefix} ──► ${targetUrl}`);
proxyConfig[service.prefix] = {
target: targetUrl,
changeOrigin: true,
// パス書き換えが必要な場合の動的制御
rewrite: (path: string) => path,
// プロキシエラー発生時の堅牢なハンドリング(バックエンド未起動時のクラッシュを防ぐ)
onError: (err: Error, req: any, res: any) => {
console.error(`[Vite Proxy Error] Failed to proxy ${req.url} to ${targetUrl}:`, err.message);
res.writeHead(502, {
‘Content-Type’: ‘application/json’,
});
res.end(JSON.stringify({
error: ‘Vite Dev Server Proxy Error’,
message: `Target backend for [${service.prefix}] at ${targetUrl} is unreachable. Please ensure the service is running.`,
details: err.message
}));
},
// WebSocketのプロキシが必要な場合(HMRやリアルタイム通信)
ws: true,
};
});
return {
plugins: [react()],
server: {
port: 3000,
host: true, // 外部からのアクセス(スマホ実機テスト等)を許可
proxy: proxyConfig,
},
};
})
—
3. DevOps的深掘り:CI/CD・Docker環境との完全統合
この動的プロキシ機構の真価は、ローカル開発だけでなく、CI/CDパイプラインやDockerベースのマルチコンテナ環境(Devcontainers等)と組み合わせたときに発揮される。
Docker Composeとのシームレスな連携
Docker Compose環境では、サービス名がそのままDNS解決される(例: `http://api-service:8080`)。しかし、ローカルのネイティブNode.jsから直接バックエンドコンテナを叩きたい場合や、その逆のケースもある。
先ほどのスクリプト内の `resolveHostTarget()` と `.dev-ports.json` の組み合わせにより、以下のワークフローが完全に自動化される。
1. Docker Compose起動時: バックエンドコンテナがアロケートした動的ポートをフックし、プロジェクトルートに `.dev-ports.json` を書き出すスクリプトを `entrypoint` に仕込む。
2. Vite起動時: `vite.config.ts` が瞬時にそのJSONを読み取り、プロキシ先をコンテナ間通信用のホスト名(例: `http://auth-container:4001`)に自動書き換えする。
CI環境(E2Eテスト時)での挙動保証
PlaywrightやCypressなどのE2EテストをCI(GitHub Actions等)で実行する際、Viteのプレビューサーバーや開発サーバーを立ち上げてテストを行うケースが多い。
バックエンドのモックサーバー(MSWや独立したGo製モック)がランダムポートで立ち上がる場合でも、CIのワークフロー内で環境変数をエクスポートしておけば、コードを一切変更することなくViteがプロキシ先を自動追従する。
GitHub Actions のワークフロー例
- name: Run E2E Tests
env:
VITE_API_PORT: ${{ steps.mock-server.outputs.assigned_port }}
run: npx playwright test
この設計により、「ローカルでは動くがCIや別人の環境ではCORSやルーティングエラーで落ちる」という、開発現場の不毛なデバッグ時間を完全にゼロに収束させることができる。
—
4. エキスパート向けパフォーマンス・最適化ハック
設定の動的化やミドルウェアの拡張を行う際、パフォーマンスの劣化を懸念するシニアエンジニアも多いだろう。ここでViteの低レイヤを最適化するための知見を共有する。
1. `fs.existsSync` や `fs.readFileSync` の同期的I/Oの最小化
`vite.config.ts` はビルドプロセス/開発サーバー起動時に一度だけ評価(Evaluation)される。したがって、リクエストごとにファイルI/Oが発生するわけではない。そのため、起動時のファイル読み込みはパフォーマンス上のボトルネックにはならない。ただし、巨大なJSONや無駄なファイルウォッッチを巻き込まないよう、パスは絶対パスでピンポイントに指定すること。
2. `http-proxy` のエージェント・プーリング最適化
大量の非同期リクエストや、フロントエンドからのポーリング(GraphQL SubscriptionsやServer-Sent Events)がプロキシを通過する場合、TCPコネクションの確立・破棄がボトルネックになる。
プロキシオプションにカスタムの `agent` を渡すことで、Keep-Aliveの効率を極限まで高めることができる。
import http from ‘node:http’;
// コネクションプールの維持によりオーバーヘッドを削減
const httpAgent = new http.Agent({ keepAlive: true, maxSockets: 100 });
proxyConfig[service.prefix] = {
target: targetUrl,
changeOrigin: true,
agent: httpAgent, // ──► ここにカスタムエージェントを注入
};
3. プロキシエラー(502)の優雅なキャッチによるDX向上
バックエンドが落ちているときに、単なるブラウザの「Network Error」やViteのコンソール上のスタックトレースだけで終わらせず、上記コードのように `onError` フックでカスタムJSONレスポンスを返すと、フロントエンド側のエラーバウンダリで「バックエンドサービスが停止しています(Port: XXXX)」という親切なUIトーストを表示させることが可能になる。
これにより、フロントエンドエンジニアがバックエンドの起動忘れに即座に気づけるという、極上のDeveloper Experience(DX)が実現する。
—
結び
開発環境の構成管理を怠ることは、チーム全体の生産性を毎日少しずつ削ぎ落とすことに等しい。「ハードコーディングされた設定」という悪習を断ち切り、環境の変動をコードが自律的に検知して適応するアーキテクチャを構築することこそ、真にスケーラブルな開発基盤を支えるDevOpsエンジニアの責務である。
今すぐあなたのプロジェクトの `vite.config.ts` を開き、その静的なプロキシ定義を自律型のエンジンへと進化させてほしい。