Express.js

Node.jsベースの最小限で柔軟なWebアプリケーションフレームワーク。軽量で高速、豊富なミドルウェアエコシステムを提供。イベント駆動型アーキテクチャ。

アプリケーションサーバーJavaScriptNode.jsWebフレームワークREST APIマイクロサービスミドルウェア

アプリケーションサーバー

Express.js

概要

Express.jsは、Node.jsベースの最小限で柔軟なWebアプリケーションフレームワークです。「高速、非意見的、最小限」をモットーとし、Webアプリケーションやモバイルアプリケーション向けの堅牢な機能セットを提供します。Node.jsフレームワークで最も広く使用されており、1,800万以上のnpmパッケージエコシステムの恩恵を受けて、初心者から経験者まで幅広く選択されています。

詳細

Express.jsは2010年にリリースされ、Node.jsエコシステムにおけるWebフレームワークのデファクトスタンダードとなっています。軽量で柔軟性が高く、豊富なミドルウェアエコシステムを持つことから、RESTful APIの構築、Webアプリケーション開発、マイクロサービスアーキテクチャまで幅広い用途で採用されています。

主要な技術的特徴

  • 最小限のコア: 必要最小限の機能のみを提供し、拡張性を重視
  • 豊富なミドルウェア: ルーティング、認証、ログ、エラーハンドリングなど
  • 高速性能: Node.jsのイベント駆動型アーキテクチャを活用
  • 柔軟なルーティング: RESTful APIの構築に最適
  • テンプレートエンジン: Pug、EJS、Handlebarsなど多様なテンプレートをサポート

用途

  • RESTful API開発
  • Webアプリケーション構築
  • マイクロサービスアーキテクチャ
  • リアルタイムアプリケーション
  • GraphQLサーバー
  • プロトタイプ開発

メリット・デメリット

メリット

  • 学習コストの低さ: シンプルなAPIで習得しやすい
  • 高いパフォーマンス: イベント駆動型で非同期処理に優秀
  • 豊富なエコシステム: npmパッケージの恩恵を最大限活用
  • 軽量: メモリ使用量が少なく、サーバーリソースを節約
  • 活発なコミュニティ: 大規模なコミュニティによるサポート
  • クロスプラットフォーム: Windows、Linux、macOSで動作

デメリット

  • シングルスレッド: CPU集約的な処理には不向き
  • コールバック地獄: 非同期処理の複雑さ(async/awaitで改善)
  • 型安全性: JavaScriptの動的型付けによる実行時エラーのリスク
  • 構造の自由度: プロジェクト構造の一貫性維持が困難
  • セキュリティ: 適切なミドルウェア選択が重要

インストール・基本設定

前提条件

# Node.jsのインストール確認
node --version
npm --version

Express.jsのインストール

# 新しいプロジェクトの初期化
mkdir myapp
cd myapp
npm init -y

# Express.jsのインストール
npm install express

# 開発時に便利なツール
npm install --save-dev nodemon

基本的なアプリケーション設定

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

// ミドルウェアの設定
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 基本的なルート
app.get('/', (req, res) => {
  res.send('Hello Express!');
});

// サーバー起動
app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

設定例

環境設定ファイル

// config/config.js
module.exports = {
  development: {
    port: process.env.PORT || 3000,
    database: 'mongodb://localhost:27017/myapp_dev',
    jwt_secret: 'your-secret-key'
  },
  production: {
    port: process.env.PORT || 80,
    database: process.env.DATABASE_URL,
    jwt_secret: process.env.JWT_SECRET
  }
};

ミドルウェアの詳細設定

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');

const app = express();

// セキュリティミドルウェア
app.use(helmet());

// CORS設定
app.use(cors({
  origin: ['http://localhost:3000', 'https://yourdomain.com'],
  credentials: true
}));

// ログ設定
app.use(morgan('combined'));

// レート制限
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100, // 最大100リクエスト
  message: 'Too many requests from this IP'
});
app.use('/api/', limiter);

// ボディパーサー
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

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

RESTful APIの実装

const express = require('express');
const router = express.Router();

// ユーザー管理API
router.get('/users', async (req, res) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

router.get('/users/:id', async (req, res) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

router.post('/users', async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

router.put('/users/:id', async (req, res) => {
  try {
    const user = await User.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.json(user);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

router.delete('/users/:id', async (req, res) => {
  try {
    const user = await User.findByIdAndDelete(req.params.id);
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.status(204).send();
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

module.exports = router;

エラーハンドリング

// カスタムエラーミドルウェア
const errorHandler = (err, req, res, next) => {
  console.error('Error:', err);

  // Mongoose バリデーションエラー
  if (err.name === 'ValidationError') {
    const errors = Object.values(err.errors).map(e => e.message);
    return res.status(400).json({
      error: 'Validation Error',
      details: errors
    });
  }

  // MongoDB 重複エラー
  if (err.code === 11000) {
    return res.status(400).json({
      error: 'Duplicate Entry',
      field: Object.keys(err.keyPattern)[0]
    });
  }

  // JWT エラー
  if (err.name === 'JsonWebTokenError') {
    return res.status(401).json({
      error: 'Invalid token'
    });
  }

  // デフォルトエラー
  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error'
  });
};

// 404ハンドラー
const notFound = (req, res, next) => {
  res.status(404).json({
    error: `Route ${req.originalUrl} not found`
  });
};

// ミドルウェアとして追加
app.use(notFound);
app.use(errorHandler);

パフォーマンス最適化

クラスタリング

// cluster.js
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);

  // CPUコア数分のワーカーを起動
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork(); // 新しいワーカーを起動
  });
} else {
  // ワーカープロセスでExpressアプリを実行
  require('./app.js');
  console.log(`Worker ${process.pid} started`);
}

圧縮とキャッシュ

const compression = require('compression');
const apicache = require('apicache');

// Gzip圧縮
app.use(compression());

// APIレスポンスキャッシュ
const cache = apicache.middleware;
app.use('/api/static-data', cache('5 minutes'));

// 条件付きキャッシュ
const onlyStatus200 = (req, res) => res.statusCode === 200;
app.use(cache('2 minutes', onlyStatus200));

接続プールとデータベース最適化

const mongoose = require('mongoose');

// MongoDB接続設定
mongoose.connect(process.env.DATABASE_URL, {
  maxPoolSize: 10, // 最大接続数
  serverSelectionTimeoutMS: 5000, // タイムアウト
  socketTimeoutMS: 45000,
  bufferCommands: false,
  bufferMaxEntries: 0
});

// 接続イベント
mongoose.connection.on('connected', () => {
  console.log('MongoDB connected');
});

mongoose.connection.on('error', (err) => {
  console.error('MongoDB connection error:', err);
});

本番環境設定

PM2設定

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'myapp',
    script: './app.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'development',
      PORT: 3000
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 80
    },
    log_file: './logs/combined.log',
    out_file: './logs/out.log',
    error_file: './logs/error.log',
    time: true
  }]
};

Docker設定

# Dockerfile
FROM node:18-alpine

WORKDIR /app

# 依存関係のインストール
COPY package*.json ./
RUN npm ci --only=production

# アプリケーションファイルのコピー
COPY . .

# 非rootユーザーの作成
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
USER nodejs

EXPOSE 3000

CMD ["node", "app.js"]

環境変数設定

# .env.production
NODE_ENV=production
PORT=3000
DATABASE_URL=mongodb://username:password@host:port/database
JWT_SECRET=your-super-secret-jwt-key
API_RATE_LIMIT_MAX=1000
REDIS_URL=redis://localhost:6379

セキュリティ対策

認証・認可の実装

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// JWT認証ミドルウェア
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.sendStatus(401);
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
};

// パスワードハッシュ化
const hashPassword = async (password) => {
  const saltRounds = 12;
  return await bcrypt.hash(password, saltRounds);
};

// ログイン処理
app.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await User.findOne({ email });
    
    if (!user || !(await bcrypt.compare(password, user.password))) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    const token = jwt.sign(
      { userId: user._id, email: user.email },
      process.env.JWT_SECRET,
      { expiresIn: '24h' }
    );

    res.json({ token });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

入力検証とサニタイゼーション

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

// バリデーションルール
const userValidationRules = () => {
  return [
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }).matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
    body('name').trim().isLength({ min: 2, max: 50 }).escape()
  ];
};

// バリデーション結果チェック
const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: 'Validation failed',
      details: errors.array()
    });
  }
  next();
};

// 使用例
app.post('/users', userValidationRules(), validate, async (req, res) => {
  // バリデーションが通過した場合の処理
});

モニタリング・ログ

ヘルスチェック

app.get('/health', (req, res) => {
  const healthcheck = {
    uptime: process.uptime(),
    message: 'OK',
    timestamp: Date.now(),
    environment: process.env.NODE_ENV,
    version: process.env.npm_package_version
  };
  res.status(200).json(healthcheck);
});

// 詳細ヘルスチェック
app.get('/health/detailed', async (req, res) => {
  try {
    // データベース接続チェック
    await mongoose.connection.db.admin().ping();
    
    const status = {
      status: 'healthy',
      timestamp: new Date().toISOString(),
      services: {
        database: 'connected',
        memory: {
          used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
          total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024)
        },
        uptime: process.uptime()
      }
    };
    res.status(200).json(status);
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message
    });
  }
});

参考ページ