TechHub

エンジニアの成長をサポートする技術情報サイト

← 記事一覧に戻る

Node.jsとExpress.jsでRESTful APIを構築する方法

公開日: 2024年2月6日 著者: mogura
Node.jsとExpress.jsでRESTful APIを構築する方法

疑問

Node.jsとExpress.jsを使ってRESTful APIを構築するには、どのように実装すればよいのでしょうか?基本的な設定から、ミドルウェア、認証まで一緒に学んでいきましょう。

導入

Node.jsとExpress.jsは、JavaScriptでサーバーサイドアプリケーションを構築するための人気の高いフレームワークです。RESTful APIの構築に最適で、豊富なエコシステムと柔軟性を提供します。

本記事では、Express.jsを使ったRESTful APIの構築方法から、ミドルウェアの活用、エラーハンドリング、認証の実装まで、実践的なコード例とともに詳しく解説していきます。

Node.jsとExpress.jsのイメージ

解説

1. プロジェクトのセットアップ

Node.jsとExpress.jsを使用してRESTful APIを構築するには、まずプロジェクトの初期設定を行う必要があります。package.jsonの設定、必要なパッケージのインストール、基本的なディレクトリ構造の作成を行います。

プロジェクトのセットアップでは、Node.jsプロジェクトの初期化、Express.jsのインストール、基本的なディレクトリ構造の作成を行います。

初期設定

npm initコマンドでプロジェクトを初期化し、package.jsonを作成します。その後、npm install expressでExpress.jsをインストールします。開発時に便利なnodemonもインストールしておくと良いでしょう。

package.jsonの設定

package.jsonにstartとdevスクリプトを追加します。startは本番環境用、devは開発環境用(nodemonを使用)に設定します。また、必要な依存関係をdependenciesとdevDependenciesに分けて記載します。

プロジェクトの初期化

これらのコマンドで、プロジェクトの初期化とExpress.jsのインストールを行います。

# プロジェクトの初期化
npm init -y

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

# 開発用ツールのインストール
npm install --save-dev nodemon

package.jsonの設定

package.jsonの設定例です。startとdevスクリプトを追加し、依存関係を設定しています。

{
  "name": "express-api",
  "version": "1.0.0",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

2. 基本的なExpressアプリケーション

Express.jsを使用して、最小限のサーバーアプリケーションを作成します。基本的なルーティング、ミドルウェアの使用、サーバーの起動方法を学びます。

最小限のサーバー

Express.jsを使用して、最小限のサーバーを作成します。app.listen()でサーバーを起動し、基本的なルーティングを設定します。JSONボディのパースにはexpress.json()ミドルウェアを使用します。

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

この例では、Express.jsを使用して最小限のサーバーを作成しています。express.json()でJSONボディをパースし、基本的なルートを設定しています。

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// JSONボディのパース
app.use(express.json());

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

// サーバーの起動
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

3. RESTful APIの実装

Express.jsを使用して、RESTful APIを実装します。ユーザー管理APIを例に、CRUD操作(作成、読み取り、更新、削除)を実装します。

ユーザー管理API

ユーザー管理APIでは、GET /usersでユーザー一覧を取得、GET /users/:idで特定のユーザーを取得、POST /usersで新規ユーザーを作成、PUT /users/:idでユーザーを更新、DELETE /users/:idでユーザーを削除します。

ルートの登録

ルートは、app.get()、app.post()、app.put()、app.delete()などのメソッドで登録します。ルートパラメータは:paramで指定し、req.paramsで取得できます。リクエストボディはreq.bodyで取得できます。

RESTful APIの実装例

この例では、ユーザー管理APIのCRUD操作を実装しています。GET、POST、PUT、DELETEメソッドを使用して、ユーザーの作成、読み取り、更新、削除を行います。

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

// 仮のデータストア
let users = [
  { id: 1, name: '山田太郎', email: 'yamada@example.com' },
  { id: 2, name: '佐藤花子', email: 'sato@example.com' }
];

// ユーザー一覧を取得
app.get('/users', (req, res) => {
  res.json(users);
});

// 特定のユーザーを取得
app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

// 新規ユーザーを作成
app.post('/users', (req, res) => {
  const { name, email } = req.body;
  const newUser = {
    id: users.length + 1,
    name,
    email
  };
  users.push(newUser);
  res.status(201).json(newUser);
});

// ユーザーを更新
app.put('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  user.name = req.body.name || user.name;
  user.email = req.body.email || user.email;
  res.json(user);
});

// ユーザーを削除
app.delete('/users/:id', (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: 'User not found' });
  }
  users.splice(index, 1);
  res.status(204).send();
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

4. ミドルウェアの活用

ミドルウェアは、リクエストとレスポンスの間に実行される関数です。ロギング、認証、エラーハンドリングなど、様々な用途で使用できます。カスタムミドルウェアを作成し、適切に活用する方法を学びます。

カスタムミドルウェア

カスタムミドルウェアは、req、res、nextを引数に取る関数です。next()を呼び出すことで、次のミドルウェアに処理を渡します。ロギング、リクエストの検証、データの変換などに使用できます。

認証ミドルウェア

認証ミドルウェアでは、リクエストヘッダーからトークンを取得し、検証します。認証が成功した場合はnext()を呼び出し、失敗した場合はエラーレスポンスを返します。

ミドルウェアの実装例

この例では、ロギングミドルウェアと認証ミドルウェアを作成しています。app.use()でグローバルに適用するか、特定のルートにのみ適用できます。

// ロギングミドルウェア
const logger = (req, res, next) => {
  console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
  next();
};

// 認証ミドルウェア
const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  // トークンの検証(簡略化)
  if (token !== 'valid-token') {
    return res.status(401).json({ error: 'Invalid token' });
  }
  req.user = { id: 1, name: 'Test User' };
  next();
};

// ミドルウェアの使用
app.use(logger);

// 保護されたルート
app.get('/protected', authenticate, (req, res) => {
  res.json({ message: 'Protected route', user: req.user });
});

5. エラーハンドリング

エラーハンドリングは、APIの堅牢性を向上させる重要な要素です。統一されたエラーレスポンス形式、エラーミドルウェアの実装、適切なHTTPステータスコードの使用を学びます。

エラークラスの作成

カスタムエラークラスを作成し、エラーの種類とメッセージを管理します。エラーミドルウェアでエラーをキャッチし、統一された形式でレスポンスを返します。

エラーハンドリングの実装例

この例では、カスタムエラークラスとエラーミドルウェアを実装しています。統一されたエラーレスポンス形式でエラーを返します。

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

// エラーミドルウェア
const errorHandler = (err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.message || 'Internal Server Error';
  
  res.status(statusCode).json({
    error: {
      code: statusCode,
      message: message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
  });
};

// エラーの使用例
app.get('/users/:id', (req, res, next) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return next(new AppError('User not found', 404));
  }
  res.json(user);
});

// エラーミドルウェアの登録(最後に配置)
app.use(errorHandler);

6. バリデーション

バリデーションは、入力データの検証を行う重要な要素です。express-validatorを使用して、リクエストデータの検証を行います。

express-validatorを使用

express-validatorを使用して、リクエストデータの検証を行います。body()、param()、query()などのメソッドで検証ルールを定義し、validationResult()で検証結果を取得します。

バリデーションの実装例

この例では、express-validatorを使用してバリデーションを実装しています。バリデーションルールを定義し、エラーがある場合は統一された形式でエラーレスポンスを返します。

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

// バリデーションルール
const validateUser = [
  body('name').trim().isLength({ min: 1, max: 100 }).withMessage('名前は1文字以上100文字以下である必要があります'),
  body('email').isEmail().normalizeEmail().withMessage('有効なメールアドレスを入力してください'),
  body('password').isLength({ min: 8 }).withMessage('パスワードは8文字以上である必要があります')
];

// バリデーションミドルウェア
const handleValidationErrors = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'バリデーションエラーが発生しました',
        details: errors.array()
      }
    });
  }
  next();
};

// ルートでの使用
app.post('/users', validateUser, handleValidationErrors, (req, res) => {
  const { name, email, password } = req.body;
  // バリデーション済みのデータを使用
  const newUser = { id: users.length + 1, name, email };
  users.push(newUser);
  res.status(201).json(newUser);
});

7. データベース統合

データベース統合は、APIの重要な要素です。MySQLやMongoDBなどのデータベースと統合し、データの永続化を行います。

MySQLとの統合

MySQLと統合するには、mysql2やsequelizeなどのパッケージを使用します。接続プールを設定し、クエリを実行します。環境変数で接続情報を管理することが重要です。

MongoDBとの統合

MongoDBと統合するには、mongooseなどのパッケージを使用します。スキーマを定義し、モデルを使用してデータを操作します。接続情報は環境変数で管理します。

MySQLとの統合例

この例では、mysql2を使用してMySQLと統合しています。接続プールを作成し、非同期でクエリを実行しています。

// MySQLとの統合(mysql2を使用)
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10
});

app.get('/users', async (req, res, next) => {
  try {
    const [rows] = await pool.query('SELECT * FROM users');
    res.json(rows);
  } catch (error) {
    next(error);
  }
});

MongoDBとの統合例

この例では、mongooseを使用してMongoDBと統合しています。スキーマを定義し、モデルを使用してデータを操作しています。

// MongoDBとの統合(mongooseを使用)
const mongoose = require('mongoose');

// スキーマ定義
const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true }
}, { timestamps: true });

const User = mongoose.model('User', userSchema);

// 接続
mongoose.connect(process.env.MONGODB_URI);

// ルートでの使用
app.get('/users', async (req, res, next) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (error) {
    next(error);
  }
});

8. 認証と認可

認証と認可は、APIのセキュリティにおいて重要な要素です。JWT(JSON Web Token)を使用した認証を実装し、保護されたルートを実装します。

JWT認証

JWTを使用した認証では、ログイン時にトークンを発行し、以降のリクエストでトークンを検証します。jsonwebtokenパッケージを使用してトークンの生成と検証を行います。

JWT認証の実装例

この例では、JWTを使用した認証を実装しています。ログイン時にトークンを発行し、認証ミドルウェアでトークンを検証します。

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

// ログイン
app.post('/auth/login', async (req, res, next) => {
  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 },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    
    res.json({ token });
  } catch (error) {
    next(error);
  }
});

// 認証ミドルウェア
const authenticateToken = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.userId = decoded.userId;
    next();
  } catch (error) {
    return res.status(403).json({ error: 'Invalid token' });
  }
};

// 保護されたルート
app.get('/users/me', authenticateToken, async (req, res, next) => {
  try {
    const user = await User.findById(req.userId);
    res.json(user);
  } catch (error) {
    next(error);
  }
});

9. レート制限

レート制限は、APIの過剰な使用を防ぎ、サーバーの負荷を軽減するための仕組みです。express-rate-limitを使用して、レート制限を実装します。

レート制限の実装

express-rate-limitを使用して、1分あたりのリクエスト数や1時間あたりのリクエスト数などのレート制限を実装します。レート制限に達した場合は、429 Too Many Requestsステータスコードを返します。

レート制限の実装例

この例では、express-rate-limitを使用してレート制限を実装しています。グローバルなレート制限と、特定のルート用のレート制限を設定しています。

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

// グローバルレート制限
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100, // 最大100リクエスト
  message: {
    error: 'Too many requests, please try again later.'
  },
  standardHeaders: true,
  legacyHeaders: false
});

app.use(limiter);

// 特定のルート用のレート制限
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // ログイン試行は5回まで
  skipSuccessfulRequests: true
});

app.use('/auth/login', authLimiter);

10. CORS設定

CORS(Cross-Origin Resource Sharing)は、異なるオリジンからのリクエストを許可するための仕組みです。corsパッケージを使用して、CORSを設定します。

CORSの設定

corsパッケージを使用して、許可するオリジン、メソッド、ヘッダーを設定します。本番環境では、特定のオリジンのみを許可することが重要です。

CORS設定の例

この例では、corsパッケージを使用してCORSを設定しています。基本的な設定と詳細な設定の両方を示しています。

const cors = require('cors');

// 基本的なCORS設定
app.use(cors());

// 詳細なCORS設定
const corsOptions = {
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true
};

app.use(cors(corsOptions));

11. 環境変数の管理

環境変数は、設定情報を管理するための重要な仕組みです。dotenvパッケージを使用して、環境変数を管理します。

dotenvの使用

dotenvパッケージを使用して、.envファイルから環境変数を読み込みます。データベース接続情報、APIキー、JWTシークレットなどの機密情報を環境変数で管理します。

環境変数の管理例

この例では、dotenvを使用して環境変数を管理しています。.envファイルから環境変数を読み込み、アプリケーションで使用します。

// .envファイル
// DB_HOST=localhost
// DB_USER=root
// DB_PASSWORD=password
// DB_NAME=mydb
// JWT_SECRET=your-secret-key
// PORT=3000

// index.js
require('dotenv').config();

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

const PORT = process.env.PORT || 3000;

// 環境変数の使用
const dbConfig = {
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME
};

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

12. ベストプラクティス

Express.jsを使用したRESTful APIの構築におけるベストプラクティスをまとめます。これらの原則に従うことで、使いやすく、保守しやすく、拡張性の高いAPIを構築できます。

  • ルートの分離: ルートを別ファイルに分離し、app.jsやindex.jsをクリーンに保ちます。ルーターを使用して、関連するルートをグループ化します。
  • ミドルウェアの適切な使用: ミドルウェアを適切に使用し、ロギング、認証、エラーハンドリングなどを実装します。ミドルウェアの順序に注意し、必要な場所に配置します。
  • エラーハンドリング: 統一されたエラーレスポンス形式を使用し、適切なHTTPステータスコードを返します。エラーミドルウェアを最後に配置し、すべてのエラーをキャッチします。
  • バリデーション: 入力データの検証を行い、不正なデータを防ぎます。express-validatorを使用して、バリデーションを実装します。
  • 環境変数の管理: 機密情報や設定情報を環境変数で管理し、.envファイルを使用します。本番環境では、環境変数を適切に設定します。
  • セキュリティ対策: HTTPSの使用、入力検証、SQLインジェクション対策、XSS対策などを実装し、安全なAPIを構築します。helmetパッケージを使用して、セキュリティヘッダーを設定します。
  • パフォーマンス最適化: キャッシング、データベースクエリの最適化、接続プールの使用などにより、APIのパフォーマンスを向上させます。
  • ロギング: 適切なロギングを実装し、エラーや重要なイベントを記録します。winstonやmorganなどのパッケージを使用します。
  • テスト: 単体テスト、統合テスト、E2Eテストを実装し、APIの品質を保証します。JestやSupertestなどのパッケージを使用します。
  • APIドキュメント: OpenAPI(Swagger)を使用して、APIをドキュメント化します。これにより、開発者がAPIを理解しやすくなります。

まとめ

Node.jsとExpress.jsを使用することで、効率的にRESTful APIを構築できます。ルートの分離、ミドルウェアの活用、適切なエラーハンドリングが重要です。

認証、バリデーション、データベース統合などの機能を適切に実装することで、本番環境で使用できる堅牢なAPIを構築できます。セキュリティとパフォーマンスも考慮し、継続的に改善していくことが大切です。

実践的なプロジェクトでExpress.jsを使用し、経験を積むことで、より効率的で保守しやすいAPIを構築できるようになります。

GraphQLとは?RESTful APIとの違いと実装方法 RESTful APIの設計原則とは?実践的なガイド