🎄 科学と神々株式会社 アドベントカレンダー 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.