0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hybrid License System Day 7: ルーティングとプロキシ実装

Last updated at Posted at 2025-12-06

🎄 科学と神々株式会社 アドベントカレンダー 2025

Hybrid License System Day 7: ルーティングとプロキシ実装

API Gateway編 (2/5)


📖 はじめに

Day 7では、ルーティングとプロキシの実装を学びます。Express.jsを使った動的ルーティング、リクエストの転送方法、エラーハンドリング、レスポンスの統一フォーマットを実装しましょう。


🚦 Express.jsルーティング基礎

基本的なルーティング

// api-gateway/src/server.js
const express = require('express');
const app = express();

// JSON parsing
app.use(express.json());

// 基本的なルート
app.get('/health', (req, res) => {
  res.json({ status: 'healthy' });
});

// パラメータ付きルート
app.get('/users/:userId', (req, res) => {
  const { userId } = req.params;
  res.json({ userId });
});

// クエリパラメータ
app.get('/search', (req, res) => {
  const { q, limit } = req.query;
  res.json({ query: q, limit: limit || 10 });
});

ルーターの分割

// api-gateway/src/routes/auth.js
const express = require('express');
const router = express.Router();

// /api/v1/license/* のルート
router.post('/activate', handleActivate);
router.get('/validate', handleValidate);

module.exports = router;
// api-gateway/src/server.js
const authRoutes = require('./routes/auth');
const adminRoutes = require('./routes/admin');

// ルーターをマウント
app.use('/api/v1/license', authRoutes);
app.use('/api/v1/admin', adminRoutes);

🔀 HTTPプロキシの実装

axiosによるプロキシ

// api-gateway/src/routes/auth.js
const axios = require('axios');

const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL || 'http://localhost:3001';

router.post('/activate', async (req, res) => {
  try {
    // リクエストをAuth Serviceに転送
    const response = await axios.post(
      `${AUTH_SERVICE_URL}/activate`,
      req.body,  // リクエストボディをそのまま転送
      {
        headers: {
          'Content-Type': 'application/json',
          'X-Service-Secret': process.env.SERVICE_SECRET,
          'X-Request-ID': req.id,
          // クライアントのIPアドレスを転送
          'X-Forwarded-For': req.ip,
          // 元のホストを転送
          'X-Forwarded-Host': req.hostname
        },
        timeout: 5000
      }
    );

    // レスポンスをクライアントに返す
    res.status(response.status).json(response.data);
  } catch (error) {
    handleProxyError(error, res);
  }
});

http-proxy-middlewareによるプロキシ

// より高度なプロキシ(オプション)
const { createProxyMiddleware } = require('http-proxy-middleware');

// Auth Serviceへのプロキシ設定
const authProxyMiddleware = createProxyMiddleware({
  target: AUTH_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: {
    '^/api/v1/license': ''  // パスを書き換え
  },
  onProxyReq: (proxyReq, req, res) => {
    // プロキシリクエストに追加ヘッダー
    proxyReq.setHeader('X-Service-Secret', process.env.SERVICE_SECRET);
    proxyReq.setHeader('X-Request-ID', req.id);
  },
  onProxyRes: (proxyRes, req, res) => {
    // プロキシレスポンスを加工
    proxyRes.headers['X-Proxy-By'] = 'API-Gateway';
  },
  onError: (err, req, res) => {
    res.status(503).json({
      error: 'Service unavailable',
      message: err.message
    });
  }
});

app.use('/api/v1/license', authProxyMiddleware);

🛠️ エラーハンドリング

包括的なエラーハンドラー

// api-gateway/src/middleware/errorHandler.js
function handleProxyError(error, res) {
  // ログ記録
  console.error({
    timestamp: new Date().toISOString(),
    error: error.message,
    stack: error.stack,
    url: error.config?.url,
    method: error.config?.method
  });

  if (error.response) {
    // バックエンドサービスがエラーレスポンスを返した(4xx, 5xx)
    const { status, data } = error.response;

    // エラーレスポンスをそのまま転送
    return res.status(status).json(data);
  }

  if (error.request) {
    // バックエンドサービスが応答しなかった(タイムアウト、接続エラー)
    if (error.code === 'ECONNREFUSED') {
      return res.status(503).json({
        error: 'Service unavailable',
        message: 'Backend service is not running',
        code: 'SERVICE_DOWN',
        retryAfter: 30
      });
    }

    if (error.code === 'ETIMEDOUT') {
      return res.status(504).json({
        error: 'Gateway timeout',
        message: 'Backend service did not respond in time',
        code: 'TIMEOUT',
        retryAfter: 10
      });
    }

    return res.status(503).json({
      error: 'Service unavailable',
      message: 'Failed to reach backend service',
      code: 'CONNECTION_ERROR'
    });
  }

  // リクエスト設定エラー
  return res.status(500).json({
    error: 'Internal server error',
    message: 'Gateway configuration error',
    code: 'GATEWAY_ERROR'
  });
}

module.exports = { handleProxyError };

グローバルエラーハンドラー

// api-gateway/src/server.js

// すべてのルートの最後に設定
app.use((err, req, res, next) => {
  console.error({
    timestamp: new Date().toISOString(),
    requestId: req.id,
    error: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method
  });

  res.status(err.status || 500).json({
    error: err.message || 'Internal server error',
    requestId: req.id,
    timestamp: new Date().toISOString()
  });
});

// 404ハンドラー
app.use((req, res) => {
  res.status(404).json({
    error: 'Not found',
    message: `Route ${req.method} ${req.path} not found`,
    requestId: req.id
  });
});

📋 レスポンス統一フォーマット

成功レスポンス

// api-gateway/src/middleware/responseFormatter.js
function formatSuccessResponse(data, meta = {}) {
  return {
    success: true,
    data,
    meta: {
      timestamp: new Date().toISOString(),
      ...meta
    }
  };
}

// 使用例
router.get('/stats', async (req, res) => {
  const stats = await getStats();

  res.json(formatSuccessResponse(stats, {
    cached: false,
    ttl: 60
  }));
});

// レスポンス例
// {
//   "success": true,
//   "data": {
//     "totalUsers": 1234,
//     "activeLicenses": 567
//   },
//   "meta": {
//     "timestamp": "2025-11-21T12:00:00.000Z",
//     "cached": false,
//     "ttl": 60
//   }
// }

エラーレスポンス

function formatErrorResponse(error, code, statusCode) {
  return {
    success: false,
    error: {
      message: error.message || error,
      code: code || 'UNKNOWN_ERROR',
      statusCode: statusCode || 500
    },
    meta: {
      timestamp: new Date().toISOString()
    }
  };
}

// 使用例
router.post('/activate', async (req, res) => {
  try {
    const result = await activateLicense(req.body);
    res.json(formatSuccessResponse(result));
  } catch (error) {
    const errorResponse = formatErrorResponse(error, 'ACTIVATION_FAILED', 400);
    res.status(400).json(errorResponse);
  }
});

// エラーレスポンス例
// {
//   "success": false,
//   "error": {
//     "message": "Invalid credentials",
//     "code": "ACTIVATION_FAILED",
//     "statusCode": 400
//   },
//   "meta": {
//     "timestamp": "2025-11-21T12:00:00.000Z"
//   }
// }

🎯 リクエスト/レスポンス変換

リクエスト変換

// クライアントのリクエストをバックエンド用に変換
router.post('/activate', async (req, res) => {
  // クライアントのリクエストボディ
  const { email, password, device_id } = req.body;

  // バックエンド用に変換
  const backendRequest = {
    email,
    password,
    clientId: device_id,  // フィールド名を変換
    source: 'api-gateway',
    timestamp: new Date().toISOString()
  };

  const response = await axios.post(`${AUTH_SERVICE_URL}/activate`, backendRequest);

  res.json(response.data);
});

レスポンス変換

// バックエンドのレスポンスをクライアント用に変換
router.get('/users/:userId', async (req, res) => {
  const { userId } = req.params;

  // Auth Serviceからユーザー情報取得
  const authResponse = await axios.get(`${AUTH_SERVICE_URL}/users/${userId}`);

  // Admin Serviceから統計情報取得
  const adminResponse = await axios.get(`${ADMIN_SERVICE_URL}/users/${userId}/stats`);

  // 2つのレスポンスを統合
  const combinedResponse = {
    user: {
      id: authResponse.data.user_id,
      email: authResponse.data.email,
      plan: authResponse.data.plan,
      memberSince: authResponse.data.created_at
    },
    statistics: {
      totalActivations: adminResponse.data.activation_count,
      lastActivation: adminResponse.data.last_activation
    }
  };

  res.json(formatSuccessResponse(combinedResponse));
});

📊 リクエストロギング

詳細なロギング

// api-gateway/src/middleware/logger.js
const morgan = require('morgan');

// カスタムフォーマット
morgan.token('request-id', (req) => req.id);
morgan.token('user-id', (req) => req.user?.userId || '-');

const logFormat = ':method :url :status :response-time ms - :request-id - :user-id';

app.use(morgan(logFormat));

// さらに詳細なロギング
app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;

    console.log({
      timestamp: new Date().toISOString(),
      requestId: req.id,
      method: req.method,
      path: req.path,
      statusCode: res.statusCode,
      duration: `${duration}ms`,
      ip: req.ip,
      userAgent: req.get('user-agent'),
      userId: req.user?.userId || null
    });
  });

  next();
});

🔄 リクエストIDの伝播

リクエストIDの生成と伝播

// api-gateway/src/middleware/requestId.js
const { v4: uuidv4 } = require('uuid');

function requestIdMiddleware(req, res, next) {
  // クライアントからX-Request-IDが送られていればそれを使用
  req.id = req.headers['x-request-id'] || uuidv4();

  // レスポンスヘッダーに含める
  res.setHeader('X-Request-ID', req.id);

  next();
}

app.use(requestIdMiddleware);

バックエンドへの伝播

// すべてのバックエンドリクエストにX-Request-IDを含める
const response = await axios.post(url, data, {
  headers: {
    'X-Request-ID': req.id,
    'X-Service-Secret': process.env.SERVICE_SECRET
  }
});

Auth Service側での受け取り

// auth-service/src/standalone.js
app.use((req, res, next) => {
  // API Gatewayから送られたリクエストIDを使用
  req.id = req.headers['x-request-id'] || uuidv4();

  console.log({
    requestId: req.id,
    path: req.path,
    method: req.method
  });

  next();
});

🚀 パフォーマンス最適化

レスポンスキャッシング

// api-gateway/src/middleware/cache.js
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 60 }); // 60秒キャッシュ

function cacheMiddleware(duration) {
  return (req, res, next) => {
    // GETリクエストのみキャッシュ
    if (req.method !== 'GET') {
      return next();
    }

    const key = `__express__${req.originalUrl || req.url}`;
    const cachedResponse = cache.get(key);

    if (cachedResponse) {
      // キャッシュヒット
      res.setHeader('X-Cache', 'HIT');
      return res.json(cachedResponse);
    }

    // オリジナルのres.jsonを保存
    const originalJson = res.json.bind(res);

    // res.jsonをオーバーライド
    res.json = (body) => {
      cache.set(key, body, duration || 60);
      res.setHeader('X-Cache', 'MISS');
      return originalJson(body);
    };

    next();
  };
}

// 使用例
router.get('/stats', cacheMiddleware(300), async (req, res) => {
  // 5分間キャッシュ
  const stats = await getStats();
  res.json(stats);
});

並行リクエスト

// 複数のバックエンドサービスに並行リクエスト
router.get('/dashboard', async (req, res) => {
  try {
    const [authStats, adminStats] = await Promise.all([
      axios.get(`${AUTH_SERVICE_URL}/stats`, { timeout: 3000 }),
      axios.get(`${ADMIN_SERVICE_URL}/stats`, { timeout: 3000 })
    ]);

    res.json({
      auth: authStats.data,
      admin: adminStats.data
    });
  } catch (error) {
    handleProxyError(error, res);
  }
});

🎯 次のステップ

Day 8では、セキュリティ層の実装について学びます。CORS設定、Helmet.jsによるセキュリティヘッダー、レート制限、JWT検証ミドルウェアを実装しましょう。


🔗 関連リンク


次回予告: Day 8では、CORS設定とHelmet.jsによるセキュリティヘッダーの実装を詳しく解説します!


Copyright © 2025 Gods & Golem, Inc. All rights reserved.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?