<CodeLearn/>
Node.js レッスン5

ミドルウェア

認証、ログ、エラーハンドリングの仕組みを理解しよう

ミドルウェアとは?

ミドルウェアは、リクエストとレスポンスの間に挟まる処理関数です。 リクエストが最終的なルートハンドラに到達する前(または後)に、 ログ記録、認証チェック、データの変換などを行います。

Expressのミドルウェアは(req, res, next)の 3つの引数を受け取ります。next()を 呼ぶと次のミドルウェアに処理が移ります。

const express = require('express');
const app = express();

// ミドルウェアの基本形
function myMiddleware(req, res, next) {
  console.log('ミドルウェアが実行されました');
  // 次のミドルウェア or ルートハンドラへ
  next();
}

// すべてのリクエストに適用
app.use(myMiddleware);

// 特定のパスにのみ適用
app.use('/api', myMiddleware);

// 特定のルートにのみ適用
app.get('/secret', myMiddleware, (req, res) => {
  res.json({ message: '秘密のデータ' });
});

// リクエストの流れ:
// リクエスト → ミドルウェア1 → ミドルウェア2 → ルートハンドラ → レスポンス

ログミドルウェア

すべてのリクエストの情報を記録するログミドルウェアは、 デバッグや監視に欠かせません。自作することも、morganなどのライブラリを使うこともできます。

// 自作ログミドルウェア
function logger(req, res, next) {
  const start = Date.now();
  const timestamp = new Date().toISOString();

  // レスポンス完了時にログを出力
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(
      `[${timestamp}] ${req.method} ${req.url} ` +
      `→ ${res.statusCode} (${duration}ms)`
    );
  });

  next();
}

app.use(logger);

// 出力例:
// [2026-03-29T10:30:00.000Z] GET /api/books → 200 (5ms)
// [2026-03-29T10:30:01.000Z] POST /api/books → 201 (12ms)
// [2026-03-29T10:30:02.000Z] GET /api/books/999 → 404 (2ms)

// --- morgan(人気のログライブラリ)を使う場合 ---
// $ npm install morgan
const morgan = require('morgan');
app.use(morgan('dev'));
// 出力: GET /api/books 200 5.123 ms - 245

CORSミドルウェア

CORS(Cross-Origin Resource Sharing)は、 異なるオリジン(ドメイン)からのAPIアクセスを制御する仕組みです。 フロントエンド(localhost:3000)からバックエンド(localhost:4000)にアクセスする場合に必要です。

// 自作CORSミドルウェア
function cors(req, res, next) {
  // 許可するオリジン
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
  // 許可するHTTPメソッド
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  // 許可するヘッダー
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  // プリフライトリクエスト(OPTIONS)への応答
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }

  next();
}

app.use(cors);

// --- corsパッケージを使う場合(推奨) ---
// $ npm install cors
const corsLib = require('cors');

// すべてのオリジンを許可
app.use(corsLib());

// 特定のオリジンのみ許可
app.use(corsLib({
  origin: ['http://localhost:3000', 'https://myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true, // Cookie を含むリクエストを許可
}));

認証ミドルウェア

保護されたAPIエンドポイントには認証ミドルウェアで アクセス制御を行います。トークンベースの認証が一般的です。

// シンプルなトークン認証ミドルウェア
function authenticate(req, res, next) {
  const authHeader = req.headers['authorization'];

  if (!authHeader) {
    return res.status(401).json({
      error: '認証トークンが必要です',
    });
  }

  // "Bearer <token>" 形式からトークンを取得
  const token = authHeader.split(' ')[1];

  if (!token) {
    return res.status(401).json({
      error: 'トークンの形式が不正です',
    });
  }

  try {
    // トークンの検証(実際にはJWTライブラリを使用)
    // const decoded = jwt.verify(token, SECRET_KEY);
    const decoded = { userId: 1, role: 'admin' }; // 仮
    req.user = decoded; // リクエストにユーザー情報を追加
    next();
  } catch (err) {
    return res.status(401).json({
      error: 'トークンが無効です',
    });
  }
}

// 権限チェックミドルウェア
function authorize(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        error: 'この操作を行う権限がありません',
      });
    }
    next();
  };
}

// 公開ルート(認証不要)
app.get('/api/books', (req, res) => {
  res.json(books);
});

// 保護されたルート(認証必要)
app.post('/api/books', authenticate, (req, res) => {
  // req.user が利用可能
  res.status(201).json({ ...req.body, createdBy: req.user.userId });
});

// 管理者のみアクセス可能
app.delete('/api/books/:id',
  authenticate,
  authorize('admin'),
  (req, res) => {
    res.status(204).send();
  }
);

エラーハンドリングミドルウェア

Expressでは、4つの引数を持つミドルウェアが エラーハンドラとして認識されます。アプリ全体のエラーを一箇所で処理できます。

// カスタムエラークラス
class AppError extends Error {
  constructor(statusCode, message) {
    super(message);
    this.statusCode = statusCode;
  }
}

// ルートハンドラでエラーを投げる
app.get('/api/books/:id', (req, res, next) => {
  const book = books.find(b => b.id === Number(req.params.id));

  if (!book) {
    // next() にエラーを渡すとエラーハンドラへ
    return next(new AppError(404, '書籍が見つかりません'));
  }

  res.json(book);
});

// 存在しないルートのハンドリング
app.use((req, res, next) => {
  next(new AppError(404, `${req.method} ${req.url} は存在しません`));
});

// エラーハンドリングミドルウェア(4つの引数)
// 必ず他のルートの後に定義する!
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.message || 'サーバー内部エラー';

  // 開発環境ではスタックトレースを出力
  console.error(`[${statusCode}] ${message}`);
  if (process.env.NODE_ENV !== 'production') {
    console.error(err.stack);
  }

  res.status(statusCode).json({
    error: {
      code: statusCode,
      message,
      // 開発環境のみスタックを返す
      ...(process.env.NODE_ENV !== 'production' && {
        stack: err.stack,
      }),
    },
  });
});

ミドルウェアチェーンの全体像

実際のアプリケーションでは、複数のミドルウェアを組み合わせてリクエスト処理のパイプラインを構成します。定義順が重要です。

const express = require('express');
const cors = require('cors');
const morgan = require('morgan');

const app = express();

// === 1. 共通ミドルウェア(順番が重要!) ===
app.use(cors());                    // CORS
app.use(morgan('dev'));             // ログ
app.use(express.json());           // JSONパース
app.use(express.urlencoded({ extended: true }));

// === 2. 静的ファイル ===
app.use(express.static('public'));

// === 3. カスタムミドルウェア ===
app.use((req, res, next) => {
  req.requestTime = new Date().toISOString();
  next();
});

// === 4. ルーティング ===
app.use('/api/books', booksRouter);
app.use('/api/users', authenticate, usersRouter);

// === 5. 404ハンドラ ===
app.use((req, res, next) => {
  next(new AppError(404, 'ルートが見つかりません'));
});

// === 6. エラーハンドラ(最後に定義) ===
app.use((err, req, res, next) => {
  res.status(err.statusCode || 500).json({
    error: { message: err.message },
  });
});

app.listen(3000);

まとめ

  • ミドルウェアは(req, res, next)の形で、リクエスト処理のパイプラインを構成する
  • next()を呼ぶと次のミドルウェアへ、呼ばないとそこで処理が止まる
  • ログ、CORS、認証、バリデーションなど用途に応じたミドルウェアを作成・利用する
  • エラーハンドリングミドルウェアは4つの引数(err, req, res, next)で定義する
  • ミドルウェアの定義順序は重要 -- 共通処理、ルーティング、404、エラーハンドラの順で配置する