404 Not Found

404 Not Found


nginx

『上級者向けExpress:エラー処理とセキュリティ・ミドルウェアの実践ガイド』

1. ボブの生産危機

ボブのAPIは、運用初日から問題に見舞われた。あるユーザーが文字化けしたメールアドレスを送信したため、データベースエラーが発生し、サイト全体がダウンしてしまった。また、誰かが500MBの画像をアップロードしたことで、ディスク容量が瞬く間に満杯になってしまった。さらに、競合他社が1秒間に1,000件のリクエストを送信するスクリプトを作成したため、サーバーが502エラーを返す事態となった。

たった1つのapp.use(errorHandler)で、生産事故の80%を防ぐことができます。

TEXT
Bob Mine Clearance Timeline:
Day 1  📧 Invalid email address → Database Error → 500 Error
Day 2  🖼️ 500MB Upload → Disk Full → Service Outage
Day 3  🤖 1000 req/s → CPU 100% → 502 Bad Gateway
Day 4  🔒 Add middleware → Take Them One by One → Stable service

2. エラー処理ミドルウェア

(1) 4パラメータ署名方式

Expressは、パラメータの数によってエラー用ミドルウェアを識別します。パラメータは4つ (err, req, res, next) である必要があります。1つでも欠けていると、通常のミドルウェアとなります。

▶ 例:グローバルエラー処理ミドルウェア

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

app.get('/boom', (req, res, next) => {
  try {
    throw new Error('Blown up on purpose');
  } catch (err) {
    next(err);
  }
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    code: err.status || 500,
    data: null,
    message: err.message
  });
});

app.listen(3000);

(2) カスタムビジネスエラークラス

▶ 例:ビジネスエラーとHTTPエラーの区別

JAVASCRIPT
class AppError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} Not found`, 404);
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
  }
}

app.get('/users/:id', (req, res, next) => {
  const user = findUser(req.params.id);
  if (!user) return next(new NotFoundError('User'));
  res.json({ code: 0, data: user, message: 'ok' });
});

(3) エラー処理手法の比較

メソッド パラメータ数 スコープ ユースケース 非同期対応
app.use(errHandler) 4 すべてのグローバルエラー 最終的なキャッチオール 手動での処理が必要 next(err)
try/catch + next(err) 単一ルート 同期コード 同期のみ
express-async-errors 0 グローバル非同期エラー async/await ルーティング 自動
domain モジュール(非推奨) プロセスレベル 推奨されません
process.on('uncaughtException') プロセスレベル 最終防衛線 グローバル
プロミス .catch() 単一のプロミス 単一の非同期操作 単一

3. express-validator によるリクエストの検証

(1) 検証チェーンとミドルウェアの使い方

express-validator は validator.js をベースとしています。これは「バリデーションチェーン」を用いてルールを宣言的に定義し、バリデーションに失敗した際に自動的にエラーを収集します。

▶ 例:ユーザー登録の確認

JAVASCRIPT
const { body, validationResult } = require('express-validator');

app.post('/register',
  body('username')
    .isLength({ min: 3, max: 20 }).withMessage('The username must3-20Character')
    .isAlphanumeric().withMessage('Usernames must consist of alphanumeric characters only.'),
  body('email')
    .isEmail().withMessage('Invalid email address')
    .normalizeEmail(),
  body('password')
    .isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
    .matches(/\d/).withMessage('Passwords must contain numbers')
    .matches(/[A-Z]/).withMessage('The password must contain uppercase letters.'),
  body('age')
    .optional()
    .isInt({ min: 1, max: 150 }).withMessage('Age requirement1-150'),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        data: errors.array(),
        message: 'Request verification failed'
      });
    }
    next();
  },
  (req, res) => {
    res.json({ code: 0, data: req.body, message: 'ok' });
  }
);

(2) express-validator でよく使われるバリデータ

バリデータ 目的 関連する修飾子
isEmail() メール body('email').isEmail() .normalizeEmail()
isLength() 長さ body('name').isLength({min:2,max:50})
isInt() / isFloat() 番号 query('page').isInt({min:1}) .toInt()
isBoolean() ブール値 body('active').isBoolean() .toBoolean()
isDate() 日付 body('birthday').isDate() .toDate()
isURL() URL body('website').isURL()
isIn() 列挙 body('role').isIn(['admin','user'])
matches() 正規表現 body('code').matches(/^\d{6}$/)
isMongoId() ObjectId param('id').isMongoId()
optional() オプション body('nickname').optional().isLength({max:30}) {nullable:true}

(3) express-validator と Joi の比較

比較基準 express-validator Joi
統合方法 Native Express ミドルウェア スタンドアロンの検証ライブラリ。手動での統合が必要
構文スタイル 連鎖呼び出し、フィールドごとの定義 スキーマオブジェクト、一度きりの定義
基盤となる依存関係 validator.js カスタム実装
エラーの収集 validationResult() 自動収集 validate().error 手動処理
ユースケース Expressプロジェクトとの迅速な統合 あらゆるNode.jsプロジェクト
習得の難易度 低(validator.js に慣れている場合) 中(独自のスキーマ構文)
型変換 .toInt() .toDate() など 自動型変換
コミュニティの規模 週間ダウンロード数 約150万 週間ダウンロード数 約500万

4. multer を使ったファイルのアップロード

(1) 3つのストレージ戦略

multerには、singlearrayfieldsの3つのアップロードモードがあります。保存先としては、memoryStorage(メモリ)とdiskStorage(ディスク)が利用可能です。

▶ 例:ディスクストレージ + ファイルフィルタリング

JAVASCRIPT
const multer = require('multer');
const path = require('path');

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/');
  },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
    cb(null, uniqueName);
  }
});

const fileFilter = (req, file, cb) => {
  const allowed = /\.(jpg|jpeg|png|gif|webp)$/i;
  if (allowed.test(path.extname(file.originalname))) {
    cb(null, true);
  } else {
    cb(new Error('Supports only jpg/png/gif/webp Format'), false);
  }
};

const upload = multer({
  storage,
  fileFilter,
  limits: { fileSize: 5 * 1024 * 1024 }
});

app.post('/avatar', upload.single('avatar'), (req, res) => {
  if (!req.file) return res.status(400).json({ code: 400, data: null, message: 'Please upload the file' });
  res.json({
    code: 0,
    data: { url: `/uploads/${req.file.filename}`, size: req.file.size },
    message: 'ok'
  });
});

app.post('/photos', upload.array('photos', 9), (req, res) => {
  const urls = req.files.map(f => `/uploads/${f.filename}`);
  res.json({ code: 0, data: urls, message: 'ok' });
});

(2) マルチ設定オプションの比較

構成項目 タイプ デフォルト値 説明
storage ストレージエンジン memoryStorage ストレージエンジン multer.diskStorage({...})
dest 文字列 対象ディレクトリ(これか「storage」のいずれか) 'uploads/'
fileFilter 機能 すべて許可 ファイルフィルタコールバック (req,file,cb)=>{...}
limits.fileSize 制限なし 1ファイルあたりの最大バイト数 5*1024*1024
limits.files 無制限 アップロード可能なファイルの最大数 9
limits.fields 番号 無制限 ファイル以外のフィールドの最大数 10
limits.fieldSize 数値 1MB ファイル以外のフィールドの最大バイト数 1024*100
limits.parts 番号 無制限 マルチパートのパーツ総数 20

(3) メモリストレージとディスクストレージ

ディメンション メモリストレージ ディスクストレージ
保存場所 メモリ(バッファ) ディスク上のファイル
req.file 物件 buffer path, filename
ユースケース 小さなファイル、リアルタイム処理(例:画像の圧縮・保存) 大きなファイル、永続ストレージ
パフォーマンス 高速(ディスクI/Oなし) やや遅い(ディスクへの書き込みが必要)
メモリ関連のリスク 大容量ファイルの処理時や高同時実行環境下でOOMが発生 なし
『Lost』を再開する はい いいえ
ファイル名の制御 不要 カスタム filename コールバックが必要

5. 静的ファイルサービスの設定

(1) express.static の詳細

▶ 例:複数のディレクトリを持つ静的サービス + キャッシュ制御

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

app.use('/static', express.static('public', {
  maxAge: '7d',
  etag: true,
  lastModified: true,
  immutable: true,
  setHeaders: (res, filePath) => {
    if (filePath.endsWith('.html')) {
      res.setHeader('Cache-Control', 'no-cache');
    }
    if (filePath.match(/\.(jpg|png|gif|webp|svg)$/)) {
      res.setHeader('Cache-Control', 'public, max-age=2592000, immutable');
    }
  }
}));

app.use('/uploads', express.static('uploads', {
  maxAge: '30d',
  dotfiles: 'deny'
}));

(2) 静的サービスに関するセキュリティ上の考慮事項


6. 回答の標準形式

(1) {コード、データ、メッセージ} の仕様

▶ 例:レスポンス・ラッパー・ミドルウェア

JAVASCRIPT
const responseHandler = (req, res, next) => {
  res.success = (data = null, message = 'ok') => {
    res.json({ code: 0, data, message });
  };
  res.fail = (message = 'Operation Failed', code = -1, data = null) => {
    res.json({ code, data, message });
  };
  res.paginate = (list, total, page, pageSize) => {
    res.json({
      code: 0,
      data: { list, total, page, pageSize, totalPages: Math.ceil(total / pageSize) },
      message: 'ok'
    });
  };
  next();
};

app.use(responseHandler);

app.get('/users', (req, res) => {
  const users = getUserList();
  res.success(users);
});

app.get('/users/:id', (req, res, next) => {
  const user = findUser(req.params.id);
  if (!user) return res.fail('User does not exist', 404);
  res.success(user);
});

(2) 国際化の6つの鉄則


7. レート制限(スロットリング)

(1) express-rate-limit の設定

▶ 例:段階的なレート制限戦略

JAVASCRIPT
const rateLimit = require('express-rate-limit');

const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { code: 429, data: null, message: 'Too many requests,Please try again later.' }
});

const apiLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 30,
  message: { code: 429, data: null, message: 'API Exceeded the call limit' }
});

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true,
  message: { code: 429, data: null, message: 'Too many failed login attempts, please try again in 15 minutes' }
});

app.use(globalLimiter);
app.use('/api/', apiLimiter);
app.use('/auth/login', loginLimiter);

(2) セキュリティミドルウェアの一覧

ミドルウェア 目的 デフォルトの動作 主な設定 npm パッケージ
helmet HTTPセキュリティヘッダー 15個のセキュリティヘッダーを設定 contentSecurityPolicy, hsts helmet
express-rate-limit レート制限 windowMs, max express-rate-limit
cors ドメイン間制御 すべてのドメイン間リクエストを拒否 origin, methods, credentials cors
express-validator 入力検証 body(), query(), param() express-validator
multer ファイルのアップロード limits, fileFilter multer
express-mongo-sanitize NoSQLインジェクション $ および . を削除 express-mongo-sanitize
xss-clean XSS クリーンアップ HTML エスケープ xss-clean
hpp パラメータ汚染 最後の値を採用 whitelist hpp
compression gzip圧縮 — thresholdlevelの圧縮

8. 環境設定:dotenv

(1) dotenv の基本的な使い方

▶ 例:環境変数の階層的な読み込み

JAVASCRIPT
const dotenv = require('dotenv');
const path = require('path');

dotenv.config({ path: path.resolve(process.env.NODE_ENV ? `.env.${process.env.NODE_ENV}` : '.env') });

const config = {
  port: parseInt(process.env.PORT, 10) || 3000,
  env: process.env.NODE_ENV || 'development',
  db: {
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT, 10) || 27017,
    name: process.env.DB_NAME || 'myapp_dev'
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '7d'
  },
  upload: {
    maxFileSize: parseInt(process.env.MAX_FILE_SIZE, 10) || 5 * 1024 * 1024,
    allowedTypes: (process.env.ALLOWED_TYPES || 'jpg,jpeg,png,gif,webp').split(',')
  },
  rateLimit: {
    windowMs: parseInt(process.env.RATE_WINDOW_MS, 10) || 15 * 60 * 1000,
    max: parseInt(process.env.RATE_MAX, 10) || 100
  }
};

module.exports = config;

(2) .env ファイルのベストプラクティス

TEXT
# .env              ← Default(Development),Do not submit to Git
# .env.production   ← Production Environment,Strictly Restrict Access
# .env.test         ← Test Environment

PORT=3000
NODE_ENV=development

DB_HOST=localhost
DB_PORT=27017
DB_NAME=myapp_dev

JWT_SECRET=your-super-secret-key-change-in-production
JWT_EXPIRES_IN=7d

MAX_FILE_SIZE=5242880
ALLOWED_TYPES=jpg,jpeg,png,gif,webp

RATE_WINDOW_MS=900000
RATE_MAX=100
TEXT
# .gitignore Must include
.env
.env.*
!.env.example

9. エクスプレスリクエストの処理プロセス全体

(1) マーメイドのフローチャート

100%
flowchart TD
    A[Client Request] --> B[helmet Safety Head]
    B --> C[cors Cross-domain validation]
    C --> D[rate-limit Traffic Flow Inspection]
    D -->|429| E[Return Rate-Limiting Response]
    D -->|Through| F[express.json Analysis Body]
    F --> G[multer File Upload Processing]
    G -->|The file is too large/Format error| H[next error]
    G -->|Through| I[express-validator Verification]
    I -->|Verification Failed| J[Back 400 Validation Error]
    I -->|Through| K[Business Routing Processing]
    K -->|Business Error| L[next error]
    K -->|Success| M[Unified Response Encapsulation success/fail]
    M --> N[Back JSON Response]
    L --> O[Global Error Handling Middleware]
    H --> O
    O --> P[Standardized Error Responses code/data/message]
    P --> N

    style E fill:#f66,stroke:#333,color:#fff
    style J fill:#f66,stroke:#333,color:#fff
    style P fill:#f66,stroke:#333,color:#fff
    style N fill:#6f6,stroke:#333

(2) ミドルウェアのロード順序に関する原則


10. 包括的な例:Express API のセキュリティ設定の完了

▶ 例:本番環境向けのExpressサーバー

JAVASCRIPT
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const multer = require('multer');
const { body, param, validationResult } = require('express-validator');
const compression = require('compression');
const path = require('path');

const config = require('./config');

const app = express();

app.use(helmet());
app.use(cors({ origin: config.corsOrigin, credentials: true }));
app.use(compression({ threshold: 1024 }));
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));

const globalLimiter = rateLimit({
  windowMs: config.rateLimit.windowMs,
  max: config.rateLimit.max,
  standardHeaders: true,
  legacyHeaders: false,
  message: { code: 429, data: null, message: 'error.rate_limited' }
});
app.use(globalLimiter);

const upload = multer({
  storage: multer.diskStorage({
    destination: 'uploads/',
    filename: (req, file, cb) => {
      const ext = path.extname(file.originalname);
      cb(null, `${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
    }
  }),
  fileFilter: (req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase();
    if (config.upload.allowedTypes.some(t => `.${t}` === ext)) {
      cb(null, true);
    } else {
      cb(new Error('error.invalid_file_type'), false);
    }
  },
  limits: { fileSize: config.upload.maxFileSize, files: 5 }
});

const responseHandler = (req, res, next) => {
  res.success = (data = null, message = 'ok') => {
    res.json({ code: 0, data, message });
  };
  res.fail = (message = 'error.internal', code = -1, data = null) => {
    res.json({ code, data, message });
  };
  next();
};
app.use(responseHandler);

const validate = (rules) => [
  ...rules,
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        data: errors.array().map(e => ({ field: e.path, message: e.msg })),
        message: 'error.validation_failed'
      });
    }
    next();
  }
];

app.post('/api/users',
  validate([
    body('username').isLength({ min: 3, max: 20 }).withMessage('error.username_length'),
    body('email').isEmail().withMessage('error.invalid_email').normalizeEmail(),
    body('password').isLength({ min: 8 }).withMessage('error.password_length')
  ]),
  (req, res) => {
    const user = createUser(req.body);
    res.success(user, 'ok');
  }
);

app.post('/api/upload',
  upload.array('files', 5),
  (req, res) => {
    if (!req.files || req.files.length === 0) {
      return res.fail('error.no_file_uploaded', 400);
    }
    const urls = req.files.map(f => `/uploads/${f.filename}`);
    res.success(urls, 'ok');
  }
);

app.get('/api/users/:id',
  validate([param('id').isMongoId().withMessage('error.invalid_id')]),
  (req, res) => {
    const user = findUser(req.params.id);
    if (!user) return res.fail('error.user_not_found', 404);
    res.success(user);
  }
);

app.use('/uploads', express.static('uploads', { maxAge: '30d', dotfiles: 'deny' }));

app.use((req, res) => {
  res.status(404).json({ code: 404, data: null, message: 'error.not_found' });
});

class AppError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
    this.isOperational = true;
  }
}

app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(413).json({ code: 413, data: null, message: 'error.file_too_large' });
    }
    return res.status(400).json({ code: 400, data: null, message: 'error.upload_failed' });
  }
  const status = err.status || 500;
  const message = err.isOperational ? err.message : 'error.internal';
  if (config.env === 'development') console.error(err.stack);
  res.status(status).json({ code: status, data: null, message });
});

app.listen(config.port, () => {
  console.log(`Server running on port ${config.port} [${config.env}]`);
});

11. このレッスンのまとめ


❓ よくある質問

Q ミドルウェアとルーティングのどちらが先に実行されますか?
A 登録された順序で実行されます。app.use に登録されたグローバルミドルウェアは、ルーティングミドルウェアよりも先に実行され、ルーティング内のミドルウェアは、ルーティングで定義された順序で実行されます。
Q APIのバージョン管理はどのように実装しますか?
A 一般的な方法として、URLパス(/v1/users)、リクエストヘッダー(Accept: application/vnd.api.v1+json)、クエリパラメータ(?version=1)の3つがあります。
Q エラー処理用ミドルウェアには4つのパラメータが必要ですか?
A はい。Expressはパラメータの数によってエラー処理用ミドルウェアを識別します。パラメータのシグネチャは (err, req, res, next) でなければならず、そうでない場合は通常のミドルウェアとして扱われます。
Q リクエストのレート制限をどのように実装すればよいですか?
A express-rate-limit ミドルウェアを使用し、windowMs の時間枠と max のリクエスト数を設定し、制限を超えた場合は 429 ステータスコードを返します。
Q 静的ファイルの配信パフォーマンスを最適化するにはどうすればよいですか?
A 本番環境では、静的ファイルのホスティングにNginxまたはCDNを使用し、Expressには動的なAPIのみを処理させます。開発環境では、express.staticが提供するmaxAgeキャッシュを使用できます。

📖 まとめ

📝 練習問題

  1. 既存のExpressプロジェクトにグローバルなエラー処理ミドルウェアを追加し、ビジネスエラー(isOperational)と未知のエラーを区別するようにする。未知のエラーについては、スタックトレースを公開せずに汎用的なメッセージを返す。
  2. express-validator を使用して、ユーザー登録 API の完全な検証チェーン(ユーザー名、メールアドレス、パスワードの強度、パスワードの確認)を作成し、検証に失敗した場合はフィールドレベルのエラーの配列を返すようにしてください。
  3. プロフィール画像のアップロードを有効にするよう、multerを設定する(1ファイルのみ、2MBまで、JPGまたはPNGのみ)。アップロードに成功した場合はファイルのURLを返し、失敗した場合は具体的な失敗理由を返す。
  4. 段階的なレート制限を行うため、express-rate-limit を追加します。全体で15分ごとに100リクエスト、1分あたり30回のAPIリクエスト、および15分ごとに5回のログイン失敗を制限します。
  5. すべての環境変数とそのデフォルト値を一覧にした「.env.example」という名前のファイルを作成し、「.env」が「.gitignore」に追加されていることを確認してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%