この記事の対象読者
- 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アプリケーションフレームワークです。)
そして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に触発されたサーバーと表現した。つまり、比較的最小限で、多くの機能がプラグインとして利用可能だということだ。)
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 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. 学習ロードマップ
この記事を読んだ後、次のステップとして以下をおすすめする。
初級者向け(まずはここから)
-
Express公式ガイドを読む
- Express Getting Started
- 公式チュートリアルは最も信頼できる情報源
-
MDNのExpressチュートリアルを実践
- MDN Express Tutorial
- LocalLibraryという実践的なプロジェクトを構築
-
TypeScriptとの統合を学ぶ
- LogRocket: Express TypeScript
- 型安全なExpress開発の基礎
中級者向け(実践に進む)
-
データベース連携を学ぶ
- Sequelize(SQL)またはMongoose(MongoDB)
- ORMを使ったデータ操作
-
認証・認可を実装する
- Passport.js + JWT
- OAuth2.0(Google、GitHub連携)
-
テスト駆動開発を導入
- Jest + Supertest
- APIエンドポイントの自動テスト
上級者向け(さらに深く)
-
NestJSへの移行を検討
- NestJS Documentation
- Expressの上に構築されたオピニオン型フレームワーク
-
マイクロサービスアーキテクチャ
- API Gateway + 複数のExpressサービス
- Docker + Kubernetes
-
パフォーマンス最適化
- クラスタリング(pm2, cluster module)
- キャッシング(Redis)
- ロードバランシング
9. まとめ
この記事では、Expressについて以下を解説した。
- Expressとは何か: Node.jsのHTTPサーバーを最小限の抽象化で使いやすくする、非オピニオン型Webフレームワーク
- なぜ今でも重要か: 他のフレームワーク(NestJS、Fastifyなど)の基盤となっており、Node.jsバックエンド開発の「共通言語」
- Express 5の新機能: Promiseの自動エラーハンドリング、Node.js 18以上必須、ReDoS対策
- 基本概念: アプリケーションオブジェクト、req/res、ミドルウェアのオニオン構造
- 主要ミドルウェア: Helmet、CORS、Morgan、Rate Limit、Passport
- ユースケース: シンプルREST API、認証付きAPI、モジュラーアーキテクチャ
私の所感
正直に言うと、Expressは「退屈なフレームワーク」だ。派手な機能はない。魔法のような抽象化もない。設定ファイル一つで全部やってくれる、なんてこともない。
しかし、その「退屈さ」こそがExpressの強みだと思っている。
Expressを使うと、HTTPリクエストがどう処理されるか、ミドルウェアがどう動くか、すべてが見える。ブラックボックスがない。だからこそ、問題が起きたときにデバッグできる。他のフレームワークに移行しても、根底にある概念が同じだから対応できる。
Express 5のリリースは、このフレームワークがまだ進化し続けていることの証明だ。10年かかったが、それはコミュニティが慎重に、壊れないように進めてきた結果でもある。
もしあなたがNode.jsでバックエンド開発を始めるなら、Expressから始めることを強くおすすめする。確かに「古い」かもしれない。しかし、古いということは「枯れている」ということでもある。問題が起きても、Stack Overflowで答えが見つかる。99%のエッジケースは誰かが既に経験している。
新しいフレームワークに飛びつく前に、まずは「基礎」を固めよう。Expressはその最良の選択肢だ。
参考文献
- Express.js 公式サイト
- GitHub expressjs/express - GitHub Star 65,000以上
- Express v5 Release Blog - Express 5.0公式リリースノート(2024年10月)
- Express Migration Guide - v4→v5移行ガイド
- MDN Express Tutorial - Mozilla Developer Network
- Wikipedia: Express.js
- Helmet.js GitHub - セキュリティミドルウェア
- Better Stack: Express 5 New Features - Express 5の新機能詳細解説