T-CREATOR

Vite の Server Proxy 機能を活用した API 開発効率化

Vite の Server Proxy 機能を活用した API 開発効率化

モダンな Web アプリケーション開発において、フロントエンドとバックエンドの連携は避けて通れない重要な要素です。特に開発段階では、CORS(Cross-Origin Resource Sharing)エラーや API エンドポイントの管理に頭を悩ませた経験をお持ちの方も多いのではないでしょうか。

そんな開発者の悩みを解決してくれるのが、Vite の Server Proxy 機能です。この機能を活用することで、複雑な API 連携も驚くほどスムーズに実現できるようになります。今回は、Vite のプロキシ機能を使った効率的な API 開発手法について、実践的な設定方法から高度なテクニックまで詳しく解説していきます。

プロキシ機能の基本概念と必要性

プロキシとは何か

プロキシ(Proxy)とは、クライアントとサーバーの間に位置し、リクエストを中継する仕組みのことです。Vite のプロキシ機能は、開発サーバーがクライアントからの API リクエストを受け取り、指定されたバックエンドサーバーに転送する役割を担います。

javascript// 基本的なプロキシの仕組み
クライアント → Vite開発サーバー → バックエンドAPI
           ↑                    ↓
           ←←←← レスポンス ←←←←

CORS エラーの根本的解決

従来のフロントエンド開発では、異なるポート番号で動作する API サーバーにアクセスする際、ブラウザのセキュリティ機能によって CORS エラーが発生することがありました。

javascript// CORSエラーが発生するケース
fetch('http://localhost:3001/api/users')
  .then((response) => response.json())
  .catch((error) => {
    // Access to fetch at 'http://localhost:3001/api/users'
    // from origin 'http://localhost:5173' has been blocked by CORS policy
    console.error('CORSエラー:', error);
  });

Vite のプロキシ機能を使用することで、同一オリジンからのリクエストとして処理されるため、CORS エラーを回避できます。これにより開発効率が大幅に向上するでしょう。

開発効率向上のメリット

プロキシ機能の導入により、以下のような恩恵を受けることができます。

#メリット詳細
1CORS 問題の解消同一オリジンリクエストとして処理される
2設定の簡素化複雑な CORS 設定が不要になる
3開発体験の向上エラーに悩まされることなく開発に集中できる
4本番環境との一致実際のデプロイ環境に近い状態で開発可能

vite.config.js でのプロキシ設定手順

基本的なプロキシ設定

Vite でプロキシを設定するには、vite.config.jsファイルのserver.proxyオプションを使用します。まずは最もシンプルな設定から始めてみましょう。

typescript// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    // 基本的なプロキシ設定
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        secure: false,
      },
    },
  },
});

この設定により、​/​apiで始まるすべてのリクエストがhttp:​/​​/​localhost:3001に転送されます。changeOrigin: trueは、プロキシ先のサーバーに対して Origin ヘッダーを変更することを意味し、多くのケースで必要になります。

パスリライト機能の活用

API のパス構造が開発環境と本番環境で異なる場合、rewriteオプションを使用してパスを書き換えることができます。

typescript// パスリライトの設定例
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        secure: false,
        // /api/users を /v1/users に書き換え
        rewrite: (path) => path.replace(/^\/api/, '/v1'),
      },
    },
  },
});

この設定では、フロントエンドから​/​api​/​usersにリクエストを送ると、実際にはhttp:​/​​/​localhost:3001​/​v1​/​usersにアクセスされます。

TypeScript 型定義の追加

TypeScript プロジェクトでは、型安全性を保つためにプロキシ設定にも型定義を適用できます。

typescript// 型安全なプロキシ設定
import { defineConfig, ProxyOptions } from 'vite';
import react from '@vitejs/plugin-react';

interface ProxyConfig {
  [key: string]: string | ProxyOptions;
}

const proxyConfig: ProxyConfig = {
  '/api': {
    target: 'http://localhost:3001',
    changeOrigin: true,
    secure: false,
    ws: true, // WebSocket対応
  },
};

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: proxyConfig,
  },
});

複数 API エンドポイントの効率的な管理方法

複数サービスへのプロキシ設定

実際のプロジェクトでは、複数のマイクロサービスや API サーバーとやり取りすることが一般的です。Vite は複数のプロキシターゲットを同時に設定できます。

typescript// 複数APIエンドポイントの管理
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      // ユーザー管理API
      '/api/users': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        secure: false,
      },
      // 商品管理API
      '/api/products': {
        target: 'http://localhost:3002',
        changeOrigin: true,
        secure: false,
      },
      // 認証API
      '/auth': {
        target: 'http://localhost:3003',
        changeOrigin: true,
        secure: false,
        // 認証関連はCookieの転送が重要
        configure: (proxy, options) => {
          proxy.on('proxyReq', (proxyReq, req) => {
            console.log('認証APIへのリクエスト:', req.url);
          });
        },
      },
    },
  },
});

環境別設定の分離

開発環境、ステージング環境、本番環境で API エンドポイントが異なる場合は、環境変数を活用して動的に設定を変更できます。

typescript// 環境変数を活用した動的プロキシ設定
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig(({ mode }) => {
  // 環境変数の読み込み
  const env = loadEnv(mode, process.cwd(), '');

  return {
    plugins: [react()],
    server: {
      proxy: {
        '/api': {
          target:
            env.VITE_API_BASE_URL ||
            'http://localhost:3001',
          changeOrigin: true,
          secure: env.VITE_API_SECURE === 'true',
        },
        '/upload': {
          target:
            env.VITE_UPLOAD_SERVER ||
            'http://localhost:3004',
          changeOrigin: true,
          secure: false,
        },
      },
    },
  };
});

対応する.env.developmentファイルは以下のようになります。

bash# .env.development
VITE_API_BASE_URL=http://localhost:3001
VITE_UPLOAD_SERVER=http://localhost:3004
VITE_API_SECURE=false

プロキシルールの優先順位管理

複数のプロキシルールが競合する可能性がある場合、設定の順序に注意する必要があります。Vite は上から順番にマッチングを行うため、より具体的なパターンを先に記述しましょう。

typescript// プロキシルールの優先順位を考慮した設定
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      // より具体的なパターンを先に記述
      '/api/users/profile': {
        target: 'http://localhost:3005', // プロフィール専用サーバー
        changeOrigin: true,
      },
      '/api/users': {
        target: 'http://localhost:3001', // 一般的なユーザーAPI
        changeOrigin: true,
      },
      // 最も汎用的なパターンを最後に
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
      },
    },
  },
});

Bearer トークンの自動付与

多くの API では認証に Bearer トークンを使用します。プロキシ設定でヘッダーを自動的に追加することで、開発時の利便性を大幅に向上させることができます。

typescript// 認証ヘッダーの自動付与設定
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        secure: false,
        configure: (proxy, options) => {
          proxy.on('proxyReq', (proxyReq, req, res) => {
            // 開発用の固定トークンを自動付与
            const developmentToken =
              process.env.VITE_DEV_TOKEN ||
              'dev-token-12345';

            // Authorizationヘッダーが存在しない場合のみ追加
            if (!proxyReq.getHeader('Authorization')) {
              proxyReq.setHeader(
                'Authorization',
                `Bearer ${developmentToken}`
              );
            }

            // Content-Typeの設定
            if (
              req.method === 'POST' ||
              req.method === 'PUT'
            ) {
              proxyReq.setHeader(
                'Content-Type',
                'application/json'
              );
            }
          });
        },
      },
    },
  },
});

セッションベースの認証を使用している場合、Cookie の転送が重要になります。特に SameSite 属性や Secure 属性の処理に注意が必要です。

typescript// Cookie転送の詳細設定
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        secure: false,
        // Cookieの転送を有効化
        cookieDomainRewrite: {
          'localhost:3001': 'localhost:5173',
        },
        configure: (proxy, options) => {
          proxy.on('proxyReq', (proxyReq, req, res) => {
            // リクエスト時のCookie処理
            console.log(
              'リクエストCookie:',
              req.headers.cookie
            );
          });

          proxy.on('proxyRes', (proxyRes, req, res) => {
            // レスポンス時のSet-Cookie処理
            const setCookie =
              proxyRes.headers['set-cookie'];
            if (setCookie) {
              // SameSite属性を開発環境用に調整
              proxyRes.headers['set-cookie'] =
                setCookie.map((cookie) =>
                  cookie.replace(
                    /SameSite=None/g,
                    'SameSite=Lax'
                  )
                );
            }
          });
        },
      },
    },
  },
});

カスタム認証ミドルウェアの作成

より複雑な認証ロジックが必要な場合は、カスタムミドルウェアを作成することができます。

typescript// カスタム認証ミドルウェア
import { IncomingMessage, ServerResponse } from 'http';

const authMiddleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: () => void
) => {
  // リクエストパスが認証を必要とするかチェック
  const protectedPaths = ['/api/users', '/api/admin'];
  const needsAuth = protectedPaths.some((path) =>
    req.url?.startsWith(path)
  );

  if (needsAuth) {
    // 認証トークンの検証
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      res.statusCode = 401;
      res.end(JSON.stringify({ error: '認証が必要です' }));
      return;
    }

    // トークンの有効性チェック(簡略化)
    const token = authHeader.substring(7);
    if (token !== process.env.VITE_VALID_TOKEN) {
      res.statusCode = 403;
      res.end(
        JSON.stringify({ error: '無効なトークンです' })
      );
      return;
    }
  }

  next();
};

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        configure: (proxy, options) => {
          proxy.on('proxyReq', authMiddleware);
        },
      },
    },
  },
});

開発環境と本番環境の切り替え戦略

環境検出とフォールバック機能

開発環境と本番環境で API エンドポイントが異なる場合、適切な切り替え戦略が必要です。環境変数を活用した動的な設定変更方法を見ていきましょう。

typescript// 環境別設定の切り替え戦略
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');

  // 環境別の設定を定義
  const getProxyConfig = () => {
    switch (mode) {
      case 'development':
        return {
          '/api': {
            target: 'http://localhost:3001',
            changeOrigin: true,
            secure: false,
            // 開発環境でのログ出力
            configure: (proxy) => {
              proxy.on('proxyReq', (proxyReq, req) => {
                console.log(
                  `🔄 プロキシ: ${req.method} ${req.url}${proxyReq.host}${proxyReq.path}`
                );
              });
            },
          },
        };

      case 'staging':
        return {
          '/api': {
            target:
              env.VITE_STAGING_API_URL ||
              'https://staging-api.example.com',
            changeOrigin: true,
            secure: true,
            headers: {
              'X-Environment': 'staging',
            },
          },
        };

      default:
        // 本番環境ではプロキシを使用しない
        return {};
    }
  };

  return {
    plugins: [react()],
    server: {
      proxy: getProxyConfig(),
    },
  };
});

フロントエンド側での API 呼び出し最適化

プロキシ設定に対応した API クライアントの実装も重要です。環境に応じて適切なベース URL を使用するヘルパー関数を作成しましょう。

typescript// utils/apiClient.ts
const getApiBaseUrl = (): string => {
  // 開発環境ではプロキシを使用
  if (import.meta.env.DEV) {
    return ''; // 相対パスでプロキシ経由
  }

  // 本番環境では環境変数からAPIベースURLを取得
  return (
    import.meta.env.VITE_API_BASE_URL ||
    'https://api.example.com'
  );
};

class ApiClient {
  private baseUrl: string;

  constructor() {
    this.baseUrl = getApiBaseUrl();
  }

  async get<T>(endpoint: string): Promise<T> {
    const url = `${this.baseUrl}/api${endpoint}`;

    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
        credentials: 'include', // Cookieを含める
      });

      if (!response.ok) {
        throw new Error(`API Error: ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      console.error('API呼び出しエラー:', error);
      throw error;
    }
  }

  async post<T>(
    endpoint: string,
    data: unknown
  ): Promise<T> {
    const url = `${this.baseUrl}/api${endpoint}`;

    return fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      credentials: 'include',
      body: JSON.stringify(data),
    }).then((response) => response.json());
  }
}

export const apiClient = new ApiClient();

Docker 環境での設定

Docker Compose を使用している場合のプロキシ設定も考慮しておきましょう。

yaml# docker-compose.yml
version: '3.8'
services:
  frontend:
    build: .
    ports:
      - '5173:5173'
    environment:
      - VITE_API_BASE_URL=http://backend:3001
    depends_on:
      - backend
    networks:
      - app-network

  backend:
    image: node:18
    ports:
      - '3001:3001'
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

対応する Vite 設定では、Docker 環境を検出してプロキシターゲットを調整します。

typescript// Docker環境対応のプロキシ設定
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');
  const isDocker = process.env.DOCKER_ENV === 'true';

  return {
    plugins: [react()],
    server: {
      host: '0.0.0.0', // Docker環境では全インターフェースをバインド
      proxy: {
        '/api': {
          target: isDocker
            ? 'http://backend:3001'
            : 'http://localhost:3001',
          changeOrigin: true,
          secure: false,
        },
      },
    },
  };
});

エラーハンドリングとデバッグテクニック

プロキシエラーの監視と対処

プロキシ経由での API 呼び出しでは、様々なエラーが発生する可能性があります。適切なエラーハンドリングを実装することで、開発効率を大幅に向上させることができます。

typescript// 詳細なエラーハンドリング設定
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        secure: false,
        configure: (proxy, options) => {
          // リクエスト前の処理
          proxy.on('proxyReq', (proxyReq, req, res) => {
            console.log(
              `📤 送信: ${req.method} ${req.url}`
            );

            // リクエストタイムアウトの設定
            proxyReq.setTimeout(5000, () => {
              console.error(
                '⏰ プロキシリクエストがタイムアウトしました'
              );
              proxyReq.destroy();
            });
          });

          // レスポンス受信時の処理
          proxy.on('proxyRes', (proxyRes, req, res) => {
            const statusCode = proxyRes.statusCode;
            console.log(
              `📥 受信: ${statusCode} ${req.url}`
            );

            // エラーレスポンスの詳細ログ
            if (statusCode && statusCode >= 400) {
              console.error(
                `❌ API Error: ${statusCode} for ${req.url}`
              );
            }
          });

          // エラー発生時の処理
          proxy.on('error', (err, req, res) => {
            console.error(
              '🚨 プロキシエラー:',
              err.message
            );

            // カスタムエラーレスポンスの送信
            if (res && !res.headersSent) {
              res.writeHead(500, {
                'Content-Type': 'application/json',
              });
              res.end(
                JSON.stringify({
                  error: 'プロキシサーバーエラー',
                  message: err.message,
                  timestamp: new Date().toISOString(),
                })
              );
            }
          });

          // プロキシ先サーバーエラーの処理
          proxy.on('proxyReqError', (err, req, res) => {
            console.error(
              '🔌 バックエンド接続エラー:',
              err.message
            );

            if (res && !res.headersSent) {
              res.writeHead(503, {
                'Content-Type': 'application/json',
              });
              res.end(
                JSON.stringify({
                  error:
                    'バックエンドサーバーに接続できません',
                  details:
                    'APIサーバーが起動していることを確認してください',
                })
              );
            }
          });
        },
      },
    },
  },
});

デバッグ用のログ機能強化

開発効率を向上させるため、詳細なログ機能を実装しましょう。

typescript// デバッグ機能付きプロキシ設定
interface RequestLog {
  timestamp: string;
  method: string;
  url: string;
  headers: Record<string, string>;
  duration?: number;
}

const requestLogs: RequestLog[] = [];

const createLogger = (enabled: boolean = true) => {
  if (!enabled) return { log: () => {}, getLogs: () => [] };

  return {
    log: (log: RequestLog) => {
      requestLogs.push(log);
      console.log(
        `📋 [${log.timestamp}] ${log.method} ${log.url} ${
          log.duration ? `(${log.duration}ms)` : ''
        }`
      );
    },
    getLogs: () => requestLogs,
  };
};

export default defineConfig(({ mode }) => {
  const logger = createLogger(mode === 'development');

  return {
    plugins: [react()],
    server: {
      proxy: {
        '/api': {
          target: 'http://localhost:3001',
          changeOrigin: true,
          configure: (proxy) => {
            proxy.on('proxyReq', (proxyReq, req) => {
              const startTime = Date.now();
              req['startTime'] = startTime;

              logger.log({
                timestamp: new Date().toISOString(),
                method: req.method || 'GET',
                url: req.url || '',
                headers: req.headers as Record<
                  string,
                  string
                >,
              });
            });

            proxy.on('proxyRes', (proxyRes, req) => {
              const duration =
                Date.now() - (req['startTime'] || 0);

              logger.log({
                timestamp: new Date().toISOString(),
                method: req.method || 'GET',
                url: req.url || '',
                headers: proxyRes.headers as Record<
                  string,
                  string
                >,
                duration,
              });
            });
          },
        },
      },
    },
  };
});

パフォーマンス監視

プロキシ経由での API 呼び出しパフォーマンスを監視する機能も実装できます。

typescript// パフォーマンス監視機能
interface PerformanceMetric {
  endpoint: string;
  averageResponseTime: number;
  totalRequests: number;
  errorCount: number;
}

class ProxyPerformanceMonitor {
  private metrics = new Map<
    string,
    {
      responseTimes: number[];
      totalRequests: number;
      errorCount: number;
    }
  >();

  recordRequest(
    endpoint: string,
    responseTime: number,
    isError: boolean = false
  ) {
    if (!this.metrics.has(endpoint)) {
      this.metrics.set(endpoint, {
        responseTimes: [],
        totalRequests: 0,
        errorCount: 0,
      });
    }

    const metric = this.metrics.get(endpoint)!;
    metric.responseTimes.push(responseTime);
    metric.totalRequests++;

    if (isError) {
      metric.errorCount++;
    }

    // パフォーマンス閾値の監視
    if (responseTime > 2000) {
      console.warn(
        `⚠️ 遅いレスポンス検出: ${endpoint} (${responseTime}ms)`
      );
    }
  }

  getMetrics(): PerformanceMetric[] {
    return Array.from(this.metrics.entries()).map(
      ([endpoint, data]) => ({
        endpoint,
        averageResponseTime:
          data.responseTimes.reduce((a, b) => a + b, 0) /
          data.responseTimes.length,
        totalRequests: data.totalRequests,
        errorCount: data.errorCount,
      })
    );
  }

  printReport() {
    console.table(this.getMetrics());
  }
}

const monitor = new ProxyPerformanceMonitor();

// プロキシ設定でモニターを使用
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        configure: (proxy) => {
          proxy.on('proxyReq', (proxyReq, req) => {
            req['startTime'] = Date.now();
          });

          proxy.on('proxyRes', (proxyRes, req) => {
            const responseTime =
              Date.now() - (req['startTime'] || 0);
            const isError =
              (proxyRes.statusCode || 0) >= 400;

            monitor.recordRequest(
              req.url || '',
              responseTime,
              isError
            );
          });

          // 定期的にレポート出力
          setInterval(() => {
            monitor.printReport();
          }, 30000); // 30秒ごと
        },
      },
    },
  },
});

まとめ

Vite の Server Proxy 機能は、モダンな Web アプリケーション開発において非常に強力なツールです。この記事では、基本的な設定から高度な活用方法まで、実践的な内容を幅広くご紹介しました。

プロキシ機能を適切に活用することで、CORS エラーの解消、複数 API エンドポイントの効率的な管理、認証機能の統合、環境間での設定切り替えなど、様々な開発課題を解決できることがお分かりいただけたでしょう。

特に重要なポイントとして、以下の点を再確認しておきましょう。

#ポイント重要度
1環境別設定の分離★★★
2エラーハンドリングの実装★★★
3認証情報の適切な管理★★★
4パフォーマンス監視★★☆
5デバッグ機能の充実★★☆

これらの機能を組み合わせることで、チーム開発での生産性向上と、より安定したアプリケーション構築が実現できます。ぜひ、あなたのプロジェクトでも Vite プロキシ機能を活用して、効率的な API 開発を実践してみてください。

今後の Web アプリケーション開発がより快適で効率的になることを願っています。

関連リンク