4
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?

Expressってなんだ?〜Node.jsの事実上の標準フレームワークを完全理解〜

Last updated at Posted at 2026-01-11

この記事の対象読者

  • JavaScriptの基本文法(関数、オブジェクト、コールバック)を理解している方
  • Node.jsをインストールしてnpmコマンドを使ったことがある方
  • Web API(REST API)という言葉を聞いたことがある方
  • バックエンド開発に興味があり、最初のフレームワークを学びたい方

この記事で得られること

  • Express.jsの設計思想と「非オピニオン型フレームワーク」の意味
  • Express 5の新機能と、v4からの移行ポイント
  • ミドルウェアの仕組みを完全に理解するための実装パターン
  • 本番環境で使える設定ファイルテンプレート(開発・テスト・本番)

この記事で扱わないこと

  • Node.jsのインストール方法
  • JavaScriptの基礎文法
  • データベース(MongoDB、PostgreSQL)との接続詳細
  • フロントエンドフレームワーク(React、Vue)との統合

1. Expressとの出会い〜なぜ15年経っても現役なのか〜

「今さらExpressを勉強する意味あるの?」

2024年にバックエンド開発を始めようとする人なら、こう思うかもしれない。Next.jsのAPI Routes、Hono、Fastify、NestJS。新しいフレームワークが次々と登場する中、2010年にリリースされたExpressは「古い」と思われがちだ。

私も最初はそう思っていた。しかし、実際に複数のプロジェクトを経験して気づいたことがある。どのフレームワークを使っても、結局Expressのパターンを理解していないと話にならないのだ。

NestJSはExpressの上に構築されている。Fastifyの設計はExpressに影響を受けている。Next.jsのミドルウェアもExpress由来の概念だ。つまり、Expressを学ぶことは「Node.jsバックエンド開発の共通言語」を習得することに等しい。

Stack Overflow Developer Survey 2024によると、ExpressはReact、Next.jsに次いで3番目に人気のあるWebフレームワークだ。週に3000万以上のnpmダウンロード数を誇り、Twitter(現X)、PayPal、Uber、IBMなど、世界中の企業が本番環境で使用している。

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
(Expressは、Webおよびモバイルアプリケーションに堅牢な機能セットを提供する、最小限かつ柔軟なNode.js Webアプリケーションフレームワークです。)

Express.js 公式サイト

そして2024年10月、10年の開発期間を経てついにExpress 5.0がリリースされた。これは「Expressは終わった」という声に対する明確な回答だ。

Expressを一言で表現するなら「Node.jsのHTTPサーバーを、最小限の抽象化で使いやすくするフレームワーク」だ。派手な機能はないが、だからこそ15年間生き残ってきた。

ここまでで、Expressがなぜ今でも学ぶ価値があるのかがイメージできただろうか。次は、この技術の背景にある設計思想を理解していこう。


2. 前提知識の確認

本題に入る前に、この記事で頻出する用語を整理しておく。

2.1 Node.jsとは

Node.jsは、JavaScriptをブラウザの外で実行するためのランタイム環境だ。2009年にRyan Dahlによって作られ、サーバーサイドJavaScriptを可能にした。

従来、JavaScriptはブラウザ内でしか動かなかった。Node.jsの登場により、同じ言語でフロントエンドとバックエンドを書けるようになり、JavaScript開発者の活躍の場が大きく広がった。

2.2 ミドルウェアとは

ミドルウェア(Middleware)とは、リクエストとレスポンスの間に挟まる処理のことだ。玉ねぎの皮のように何層にも重なり、各層が特定の処理を担当する。

例えば、ログ記録、認証チェック、リクエストボディの解析などが典型的なミドルウェアだ。Expressのアーキテクチャはこのミドルウェアを中心に設計されている。

2.3 オピニオン型 vs 非オピニオン型フレームワーク

フレームワークには「オピニオン型(Opinionated)」と「非オピニオン型(Unopinionated)」の2種類がある。

オピニオン型(例: Ruby on Rails、NestJS)は「こう書くべき」という明確な規約を持つ。ファイル構成、命名規則、使うべきライブラリまで決まっている。学習コストは高いが、チーム開発で一貫性が保てる。

非オピニオン型(例: Express)は規約が最小限で、開発者の自由度が高い。ファイル構成もライブラリ選定も自分で決める。柔軟だが、経験がないと「正解がわからない」という悩みを抱えやすい。

Expressは非オピニオン型の代表だ。良くも悪くも「何も決めてくれない」フレームワークなので、自分でアーキテクチャを設計する必要がある。

2.4 HTTPメソッドとルーティング

HTTPメソッドは、クライアントがサーバーに「何をしたいか」を伝える方法だ。

メソッド 用途
GET データの取得 ユーザー一覧を取得
POST データの作成 新規ユーザーを登録
PUT データの更新(全体) ユーザー情報を全部置き換え
PATCH データの更新(部分) メールアドレスだけ変更
DELETE データの削除 ユーザーを削除

ルーティングは、特定のURLパスとHTTPメソッドの組み合わせを、対応する処理(ハンドラー)に紐づける仕組みだ。

これらの概念を押さえたところで、抽象的な概念から具体的な実装へ進んでいこう。


3. Expressが生まれた背景〜Node.jsの課題を解決する〜

3.1 Node.jsの素の実装の問題点

Node.jsには組み込みのhttpモジュールがあり、フレームワークなしでもWebサーバーを作れる。しかし、実際にやってみると冗長なコードになりがちだ。

// フレームワークなしのNode.js HTTPサーバー
const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url, true);
  const path = parsedUrl.pathname;
  const method = req.method;

  if (method === 'GET' && path === '/') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World');
  } else if (method === 'GET' && path === '/users') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify([{ id: 1, name: 'Alice' }]));
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
});

server.listen(3000);

このコードの問題点は明らかだ。

  • ルーティングがif-else文の羅列になる
  • URLパラメータ(/users/:id)の解析が手動
  • リクエストボディの解析が面倒
  • エラーハンドリングが散らばる

3.2 Expressの誕生

Expressは2010年11月にTJ Holowaychukによってリリースされた。Ruby on Railsに触発されながらも、Rubyの「Sinatra」フレームワークのようなシンプルさを目指した設計だ。

The original author, TJ Holowaychuk, described it as a Sinatra-inspired server, meaning that it is relatively minimal with many features available as plugins.
(原作者のTJ Holowaychukは、ExpressをSinatraに触発されたサーバーと表現した。つまり、比較的最小限で、多くの機能がプラグインとして利用可能だということだ。)

Wikipedia: Express.js

2014年にStrongLoopがExpressの管理権を取得し、2015年にIBMがStrongLoopを買収。現在はOpenJS Foundationの「At-Large」プロジェクトとして運営されている。

3.3 Express 5.0のリリース〜10年越しの進化〜

2024年10月15日、Express 5.0が正式リリースされた。最初のプルリクエストが2014年7月に作成されてから、実に10年以上の開発期間を経ての公開だ。

This release is designed to be boring! That may sound odd, but we've intentionally kept it simple to unblock the ecosystem and enable more impactful changes in future releases.
(このリリースは「退屈」であるように設計されている。奇妙に聞こえるかもしれないが、エコシステムのブロックを解除し、将来のリリースでより影響力のある変更を可能にするために、意図的にシンプルに保った。)

Express v5 Release Blog

Express 5の主な変更点は以下の通りだ。

変更点 内容
Node.js 18以上が必須 古いバージョンのサポートを終了し、パフォーマンス改善を実現
Promiseの自動エラーハンドリング async/awaitでtry-catchが不要に
path-to-regexpの更新 ReDoS攻撃対策のためルート構文が変更
body-parserの内蔵 別途インストール不要でJSONボディを解析可能
非推奨APIの削除 res.sendfile()res.sendFile()など

背景が理解できたところで、概念を具体的なコードで実装していこう。


4. Expressの基本概念〜3つの柱〜

4.1 アプリケーションオブジェクト

Expressアプリケーションはexpress()関数を呼び出すことで作成する。

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

このappオブジェクトが、ルーティング、ミドルウェア、設定のすべてを管理する中心的な存在だ。

4.2 リクエストとレスポンス(req, res)

Expressのルートハンドラーは、2つの主要なオブジェクトを受け取る。

app.get('/users/:id', (req, res) => {
  // req: リクエスト情報(パラメータ、クエリ、ボディなど)
  // res: レスポンスを返すためのメソッド群
  console.log(req.params.id);  // URLパラメータ
  console.log(req.query.page); // クエリパラメータ (?page=1)
  res.json({ message: 'OK' }); // JSONレスポンス
});

reqオブジェクトの主要プロパティ:

プロパティ 用途
req.params URLパラメータ(:idなど)
req.query クエリ文字列(?key=value
req.body リクエストボディ(POST/PUTデータ)
req.headers HTTPヘッダー
req.method HTTPメソッド(GET, POST等)
req.path URLパス

resオブジェクトの主要メソッド:

メソッド 用途
res.json(data) JSONレスポンスを返す
res.send(data) 様々な形式のレスポンスを返す
res.status(code) ステータスコードを設定
res.sendFile(path) ファイルを送信
res.redirect(url) リダイレクト
res.render(view) テンプレートをレンダリング

4.3 ミドルウェアの仕組み

ミドルウェアはExpressの核心だ。リクエストが来てからレスポンスを返すまでの間に、複数の処理を順番に実行する。

// ミドルウェアの基本形
const myMiddleware = (req, res, next) => {
  console.log('処理前');
  next(); // 次のミドルウェアまたはルートハンドラーへ
  console.log('処理後');
};

app.use(myMiddleware);

next()を呼ばないと、リクエストはそこで止まってしまう。逆にres.send()などでレスポンスを返すと、以降のミドルウェアは実行されない。

ミドルウェアの実行順序は「オニオン(玉ねぎ)構造」と呼ばれる。

リクエスト
    ↓
┌─ Middleware 1(前半)
│  ┌─ Middleware 2(前半)
│  │  ┌─ Middleware 3(前半)
│  │  │  ルートハンドラー
│  │  └─ Middleware 3(後半)
│  └─ Middleware 2(後半)
└─ Middleware 1(後半)
    ↓
レスポンス

基本概念を理解したところで、実際に手を動かしてExpressアプリを構築していこう。


5. 実際に使ってみよう〜環境構築から本番設定まで〜

5.1 環境構築

JavaScript版(クイックスタート)

# プロジェクト作成
mkdir my-express-app
cd my-express-app
npm init -y

# Expressをインストール
npm install express

TypeScript版(推奨)

# プロジェクト作成
mkdir my-express-app
cd my-express-app
npm init -y

# 依存関係をインストール
npm install express dotenv
npm install -D typescript @types/node @types/express ts-node nodemon

5.2 設定ファイルの準備

本番環境を見据えた設定ファイルを用意しよう。以下の5種類のテンプレートをそのままコピーして使える。

TypeScript設定(tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

開発環境用(.env.development)

# .env.development - 開発環境用(このままコピーして使える)
NODE_ENV=development
PORT=3000

# ログ設定
LOG_LEVEL=debug
LOG_FORMAT=dev

# CORS設定(開発時は緩めに)
CORS_ORIGIN=http://localhost:3001

# APIレート制限(開発時は緩めに)
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=1000

本番環境用(.env.production)

# .env.production - 本番環境用(このままコピーして使える)
NODE_ENV=production
PORT=8080

# ログ設定
LOG_LEVEL=error
LOG_FORMAT=combined

# CORS設定(本番は厳格に)
CORS_ORIGIN=https://yourdomain.com

# APIレート制限(本番は厳格に)
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=100

# セキュリティ
HELMET_ENABLED=true

テスト環境用(.env.test)

# .env.test - テスト/CI環境用(このままコピーして使える)
NODE_ENV=test
PORT=3001

# ログ設定(テスト時はノイズを減らす)
LOG_LEVEL=warn
LOG_FORMAT=dev

# CORS設定
CORS_ORIGIN=*

# APIレート制限(テスト時は無制限)
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=10000

package.json(開発/本番スクリプト設定)

{
  "name": "my-express-app",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "dev": "nodemon --exec ts-node src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "test": "jest",
    "lint": "eslint src/**/*.ts",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.4.0",
    "express": "^5.0.0",
    "express-rate-limit": "^7.0.0",
    "helmet": "^7.1.0",
    "morgan": "^1.10.0"
  },
  "devDependencies": {
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/morgan": "^1.9.9",
    "@types/node": "^20.0.0",
    "nodemon": "^3.0.0",
    "ts-node": "^10.9.0",
    "typescript": "^5.0.0"
  }
}

nodemon.json(ホットリロード設定)

{
  "watch": ["src"],
  "ext": "ts,json",
  "ignore": ["src/**/*.spec.ts"],
  "exec": "ts-node ./src/index.ts"
}

5.3 基本的なAPIサーバーの実装

設定ファイルが準備できたら、実際にコードを書いていく。以下は完全に動作するサンプルコードだ。

/**
 * src/index.ts - Express基本APIサーバー
 * 
 * 使い方:
 * - 開発: npm run dev
 * - ビルド: npm run build
 * - 本番: npm start
 */
import express, { Application, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import rateLimit from 'express-rate-limit';
import dotenv from 'dotenv';

// 環境変数の読み込み
dotenv.config({
  path: `.env.${process.env.NODE_ENV || 'development'}`
});

const app: Application = express();
const PORT = process.env.PORT || 3000;

// =============================================================================
// ミドルウェア設定
// =============================================================================

// セキュリティヘッダー(Helmet)
app.use(helmet());

// CORS設定
app.use(cors({
  origin: process.env.CORS_ORIGIN || '*',
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

// リクエストログ(Morgan)
app.use(morgan(process.env.LOG_FORMAT || 'dev'));

// JSONボディパーサー(Express 5では組み込み)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// レート制限
const limiter = rateLimit({
  windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000'),
  max: parseInt(process.env.RATE_LIMIT_MAX || '100'),
  message: { error: 'Too many requests, please try again later.' },
});
app.use('/api/', limiter);

// =============================================================================
// ルート定義
// =============================================================================

// ヘルスチェック
app.get('/health', (req: Request, res: Response) => {
  res.json({
    status: 'ok',
    timestamp: new Date().toISOString(),
    environment: process.env.NODE_ENV,
  });
});

// ルートエンドポイント
app.get('/', (req: Request, res: Response) => {
  res.json({
    message: 'Welcome to Express API',
    version: '1.0.0',
    docs: '/api-docs',
  });
});

// ユーザーAPI(サンプル)
interface User {
  id: number;
  name: string;
  email: string;
}

let users: User[] = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' },
];

// ユーザー一覧取得
app.get('/api/users', (req: Request, res: Response) => {
  const page = parseInt(req.query.page as string) || 1;
  const limit = parseInt(req.query.limit as string) || 10;
  const start = (page - 1) * limit;
  const end = start + limit;

  res.json({
    data: users.slice(start, end),
    pagination: {
      page,
      limit,
      total: users.length,
      totalPages: Math.ceil(users.length / limit),
    },
  });
});

// ユーザー詳細取得
app.get('/api/users/:id', (req: Request, res: Response) => {
  const id = parseInt(req.params.id);
  const user = users.find((u) => u.id === id);

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  res.json(user);
});

// ユーザー作成
app.post('/api/users', (req: Request, res: Response) => {
  const { name, email } = req.body;

  if (!name || !email) {
    return res.status(400).json({ error: 'name and email are required' });
  }

  const newUser: User = {
    id: users.length > 0 ? Math.max(...users.map((u) => u.id)) + 1 : 1,
    name,
    email,
  };

  users.push(newUser);
  res.status(201).json(newUser);
});

// ユーザー更新
app.put('/api/users/:id', (req: Request, res: Response) => {
  const id = parseInt(req.params.id);
  const index = users.findIndex((u) => u.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'User not found' });
  }

  const { name, email } = req.body;
  users[index] = { ...users[index], name, email };
  res.json(users[index]);
});

// ユーザー削除
app.delete('/api/users/:id', (req: Request, res: Response) => {
  const id = parseInt(req.params.id);
  const index = users.findIndex((u) => u.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'User not found' });
  }

  users.splice(index, 1);
  res.status(204).send();
});

// =============================================================================
// エラーハンドリング
// =============================================================================

// 404ハンドラー
app.use((req: Request, res: Response) => {
  res.status(404).json({
    error: 'Not Found',
    path: req.path,
    method: req.method,
  });
});

// グローバルエラーハンドラー
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error('Error:', err.stack);

  res.status(500).json({
    error: 'Internal Server Error',
    message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong',
  });
});

// =============================================================================
// サーバー起動
// =============================================================================

app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
  console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
});

export default app;

5.4 実行結果

上記のコードを実行すると、以下のような出力が得られる。

$ npm run dev

> my-express-app@1.0.0 dev
> nodemon --exec ts-node src/index.ts

[nodemon] 3.0.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/*
[nodemon] watching extensions: ts,json
[nodemon] starting `ts-node ./src/index.ts`
Server is running on http://localhost:3000
Environment: development

# 別ターミナルでAPIをテスト
$ curl http://localhost:3000/health
{
  "status": "ok",
  "timestamp": "2024-10-20T10:30:00.000Z",
  "environment": "development"
}

$ curl http://localhost:3000/api/users
{
  "data": [
    {"id":1,"name":"Alice","email":"alice@example.com"},
    {"id":2,"name":"Bob","email":"bob@example.com"}
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 2,
    "totalPages": 1
  }
}

$ curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Charlie","email":"charlie@example.com"}'
{
  "id": 3,
  "name": "Charlie",
  "email": "charlie@example.com"
}

5.5 よくあるエラーと対処法

Expressを使い始めると、いくつかのエラーに遭遇する。私自身がハマったポイントも含めて対処法をまとめた。

エラー 原因 対処法
Cannot GET / ルートが定義されていない app.get('/', ...)でルートを追加。パスのスペルミスも確認
req.body is undefined ボディパーサー未設定 app.use(express.json())を追加(Express 5では組み込み)
CORS error CORSミドルウェア未設定 npm install corsしてapp.use(cors())を追加
next is not a function ミドルウェアの引数が不足 (req, res, next) => {}の形式で定義
Error [ERR_HTTP_HEADERS_SENT] レスポンスを複数回送信 return res.json(...)でreturnを付けて早期終了
EADDRINUSE ポートが既に使用中 別のプロセスを終了するか、PORTを変更

Express 5特有の注意点:

変更点 Express 4 Express 5
オプショナルパラメータ /user/:id? /user{/:id}
ワイルドカード /foo* /foo(.*)
app.del() 使用可 app.delete()に統一
Promiseのエラー try-catch必須 自動でエラーハンドラーへ

基本的な使い方をマスターしたので、次は主要なミドルウェアとその活用法を見ていこう。


6. 主要ミドルウェア完全ガイド

Expressの真価はミドルウェアエコシステムにある。ここでは本番環境で必須の5つのミドルウェアを解説する。

6.1 Helmet(セキュリティヘッダー)

Helmetは、HTTPヘッダーを設定してWebアプリケーションを一般的な脆弱性から保護する。

npm install helmet
import helmet from 'helmet';

// 基本的な使い方(推奨設定を全て有効化)
app.use(helmet());

// カスタマイズする場合
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  crossOriginEmbedderPolicy: false, // 必要に応じて無効化
}));

Helmetが設定するヘッダー:

ヘッダー 保護対象
Content-Security-Policy XSS攻撃、データインジェクション
X-Frame-Options クリックジャッキング
X-Content-Type-Options MIMEタイプスニッフィング
Strict-Transport-Security ダウングレード攻撃

6.2 CORS(クロスオリジンリソース共有)

CORSは、異なるオリジン(ドメイン)からのリクエストを制御する。

npm install cors
import cors from 'cors';

// 全オリジンを許可(開発時のみ)
app.use(cors());

// 本番環境向けの設定
app.use(cors({
  origin: ['https://yourdomain.com', 'https://admin.yourdomain.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true, // Cookieを含むリクエストを許可
  maxAge: 86400, // プリフライトリクエストのキャッシュ(24時間)
}));

// 特定のルートだけに適用
app.get('/api/public', cors(), (req, res) => {
  res.json({ public: true });
});

6.3 Morgan(リクエストログ)

Morganは、HTTPリクエストのログを記録する。

npm install morgan
import morgan from 'morgan';
import fs from 'fs';
import path from 'path';

// 開発環境:コンソールに色付きログ
app.use(morgan('dev'));

// 本番環境:ファイルにログ出力
const accessLogStream = fs.createWriteStream(
  path.join(__dirname, '../logs/access.log'),
  { flags: 'a' }
);
app.use(morgan('combined', { stream: accessLogStream }));

// カスタムフォーマット
app.use(morgan(':method :url :status :response-time ms - :res[content-length]'));

6.4 express-rate-limit(レート制限)

APIの乱用を防ぐために、リクエスト数を制限する。

npm install express-rate-limit
import rateLimit from 'express-rate-limit';

// 基本設定(1分間に100リクエストまで)
const limiter = rateLimit({
  windowMs: 60 * 1000, // 1分
  max: 100,
  message: { error: 'Too many requests, please try again later.' },
  standardHeaders: true, // Rate-Limit-*ヘッダーを返す
  legacyHeaders: false,
});

app.use('/api/', limiter);

// ログインエンドポイント用(より厳しい制限)
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 5, // 5回まで
  message: { error: 'Too many login attempts, please try again after 15 minutes.' },
});

app.post('/api/auth/login', loginLimiter, (req, res) => {
  // ログイン処理
});

6.5 Passport(認証)

Passportは、多様な認証戦略(ローカル、OAuth、JWTなど)をサポートする。

npm install passport passport-local passport-jwt
npm install -D @types/passport @types/passport-local @types/passport-jwt
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';

// ローカル認証戦略(ユーザー名/パスワード)
passport.use(new LocalStrategy(
  async (username, password, done) => {
    try {
      const user = await findUserByUsername(username);
      if (!user) {
        return done(null, false, { message: 'User not found' });
      }
      const isValid = await comparePassword(password, user.password);
      if (!isValid) {
        return done(null, false, { message: 'Invalid password' });
      }
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// JWT認証戦略
passport.use(new JwtStrategy(
  {
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: process.env.JWT_SECRET || 'your-secret-key',
  },
  async (payload, done) => {
    try {
      const user = await findUserById(payload.sub);
      if (!user) {
        return done(null, false);
      }
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// ミドルウェアとして使用
app.use(passport.initialize());

// 認証が必要なルート
app.get('/api/protected', 
  passport.authenticate('jwt', { session: false }),
  (req, res) => {
    res.json({ message: 'Protected data', user: req.user });
  }
);

ミドルウェアの活用法を把握できたところで、実際のユースケースに当てはめていこう。


7. ユースケース別ガイド

7.1 ユースケース1: シンプルなREST APIサーバー

想定読者: バックエンド開発を始めたい初心者

推奨構成: Express + TypeScript + Nodemon

特徴: 最小限の設定で学習に集中できる

/**
 * シンプルREST API - 学習用テンプレート
 * 起動: npm run dev
 */
import express, { Request, Response } from 'express';

const app = express();
app.use(express.json());

// データストア(実際はDBを使う)
interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

let todos: Todo[] = [];
let nextId = 1;

// CRUD操作
app.get('/todos', (req: Request, res: Response) => {
  res.json(todos);
});

app.post('/todos', (req: Request, res: Response) => {
  const todo: Todo = {
    id: nextId++,
    title: req.body.title,
    completed: false,
  };
  todos.push(todo);
  res.status(201).json(todo);
});

app.patch('/todos/:id', (req: Request, res: Response) => {
  const todo = todos.find(t => t.id === parseInt(req.params.id));
  if (!todo) return res.status(404).json({ error: 'Not found' });
  
  Object.assign(todo, req.body);
  res.json(todo);
});

app.delete('/todos/:id', (req: Request, res: Response) => {
  const index = todos.findIndex(t => t.id === parseInt(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'Not found' });
  
  todos.splice(index, 1);
  res.status(204).send();
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

7.2 ユースケース2: 認証付きAPIサーバー

想定読者: セキュリティを考慮したAPI開発をしたい中級者

推奨構成: Express + Passport + JWT + bcrypt

特徴: ログイン、トークン認証、保護されたルート

/**
 * 認証付きAPI - JWTトークン認証
 */
import express, { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';

const app = express();
app.use(express.json());

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';

// 仮のユーザーDB
interface User {
  id: number;
  email: string;
  password: string;
}

const users: User[] = [];

// ユーザー登録
app.post('/auth/register', async (req: Request, res: Response) => {
  const { email, password } = req.body;
  
  if (users.find(u => u.email === email)) {
    return res.status(400).json({ error: 'Email already exists' });
  }
  
  const hashedPassword = await bcrypt.hash(password, 10);
  const user: User = {
    id: users.length + 1,
    email,
    password: hashedPassword,
  };
  
  users.push(user);
  res.status(201).json({ message: 'User registered', userId: user.id });
});

// ログイン
app.post('/auth/login', async (req: Request, res: Response) => {
  const { email, password } = req.body;
  
  const user = users.find(u => u.email === email);
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  const token = jwt.sign({ sub: user.id, email: user.email }, JWT_SECRET, {
    expiresIn: '1h',
  });
  
  res.json({ token, expiresIn: 3600 });
});

// 認証ミドルウェア
const authenticate = (req: Request, res: Response, next: NextFunction) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  const token = authHeader.substring(7);
  
  try {
    const decoded = jwt.verify(token, JWT_SECRET) as { sub: number; email: string };
    (req as any).user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// 保護されたルート
app.get('/api/profile', authenticate, (req: Request, res: Response) => {
  const user = (req as any).user;
  res.json({ userId: user.sub, email: user.email });
});

app.listen(3000);

7.3 ユースケース3: モジュラーアーキテクチャ(大規模アプリ向け)

想定読者: 保守性の高いコードを書きたい上級者

推奨構成: Express + Router + Controller + Service層

特徴: ファイル分割、依存性注入、テスタブル

src/
├── index.ts           # エントリーポイント
├── app.ts             # Expressアプリ設定
├── config/
│   └── index.ts       # 環境変数管理
├── routes/
│   ├── index.ts       # ルート集約
│   └── users.ts       # ユーザールート
├── controllers/
│   └── userController.ts
├── services/
│   └── userService.ts
├── middleware/
│   ├── auth.ts
│   └── errorHandler.ts
└── types/
    └── index.ts
// src/routes/users.ts
import { Router } from 'express';
import { UserController } from '../controllers/userController';

const router = Router();
const userController = new UserController();

router.get('/', userController.getAll);
router.get('/:id', userController.getById);
router.post('/', userController.create);
router.put('/:id', userController.update);
router.delete('/:id', userController.delete);

export default router;
// src/controllers/userController.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/userService';

export class UserController {
  private userService: UserService;

  constructor() {
    this.userService = new UserService();
  }

  getAll = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const users = await this.userService.findAll();
      res.json(users);
    } catch (error) {
      next(error);
    }
  };

  getById = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const user = await this.userService.findById(parseInt(req.params.id));
      if (!user) {
        return res.status(404).json({ error: 'User not found' });
      }
      res.json(user);
    } catch (error) {
      next(error);
    }
  };

  create = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const user = await this.userService.create(req.body);
      res.status(201).json(user);
    } catch (error) {
      next(error);
    }
  };

  update = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const user = await this.userService.update(parseInt(req.params.id), req.body);
      res.json(user);
    } catch (error) {
      next(error);
    }
  };

  delete = async (req: Request, res: Response, next: NextFunction) => {
    try {
      await this.userService.delete(parseInt(req.params.id));
      res.status(204).send();
    } catch (error) {
      next(error);
    }
  };
}
// src/app.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import userRoutes from './routes/users';
import { errorHandler } from './middleware/errorHandler';

const app = express();

// ミドルウェア
app.use(helmet());
app.use(cors());
app.use(morgan('dev'));
app.use(express.json());

// ルート
app.use('/api/users', userRoutes);

// エラーハンドラー
app.use(errorHandler);

export default app;

ユースケースが把握できたところで、この記事を読んだ後の学習パスを確認しよう。


8. 学習ロードマップ

この記事を読んだ後、次のステップとして以下をおすすめする。

初級者向け(まずはここから)

  1. Express公式ガイドを読む

  2. MDNのExpressチュートリアルを実践

  3. TypeScriptとの統合を学ぶ

中級者向け(実践に進む)

  1. データベース連携を学ぶ

    • Sequelize(SQL)またはMongoose(MongoDB)
    • ORMを使ったデータ操作
  2. 認証・認可を実装する

    • Passport.js + JWT
    • OAuth2.0(Google、GitHub連携)
  3. テスト駆動開発を導入

    • Jest + Supertest
    • APIエンドポイントの自動テスト

上級者向け(さらに深く)

  1. NestJSへの移行を検討

  2. マイクロサービスアーキテクチャ

    • API Gateway + 複数のExpressサービス
    • Docker + Kubernetes
  3. パフォーマンス最適化

    • クラスタリング(pm2, cluster module)
    • キャッシング(Redis)
    • ロードバランシング

9. まとめ

この記事では、Expressについて以下を解説した。

  1. Expressとは何か: Node.jsのHTTPサーバーを最小限の抽象化で使いやすくする、非オピニオン型Webフレームワーク
  2. なぜ今でも重要か: 他のフレームワーク(NestJS、Fastifyなど)の基盤となっており、Node.jsバックエンド開発の「共通言語」
  3. Express 5の新機能: Promiseの自動エラーハンドリング、Node.js 18以上必須、ReDoS対策
  4. 基本概念: アプリケーションオブジェクト、req/res、ミドルウェアのオニオン構造
  5. 主要ミドルウェア: Helmet、CORS、Morgan、Rate Limit、Passport
  6. ユースケース: シンプルREST API、認証付きAPI、モジュラーアーキテクチャ

私の所感

正直に言うと、Expressは「退屈なフレームワーク」だ。派手な機能はない。魔法のような抽象化もない。設定ファイル一つで全部やってくれる、なんてこともない。

しかし、その「退屈さ」こそがExpressの強みだと思っている。

Expressを使うと、HTTPリクエストがどう処理されるか、ミドルウェアがどう動くか、すべてが見える。ブラックボックスがない。だからこそ、問題が起きたときにデバッグできる。他のフレームワークに移行しても、根底にある概念が同じだから対応できる。

Express 5のリリースは、このフレームワークがまだ進化し続けていることの証明だ。10年かかったが、それはコミュニティが慎重に、壊れないように進めてきた結果でもある。

もしあなたがNode.jsでバックエンド開発を始めるなら、Expressから始めることを強くおすすめする。確かに「古い」かもしれない。しかし、古いということは「枯れている」ということでもある。問題が起きても、Stack Overflowで答えが見つかる。99%のエッジケースは誰かが既に経験している。

新しいフレームワークに飛びつく前に、まずは「基礎」を固めよう。Expressはその最良の選択肢だ。


参考文献

4
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
4
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?