NodeJS 速習チュートリアル

Node.js RESTful API

1. RESTful APIを理解する

REST(Representational State Transfer)は、ネットワークアプリケーションを設計するためのアーキテクチャスタイルであり、Webサービスの標準となっています。
RESTful APIは、アプリケーションを統合し、異なるシステム間での通信を可能にするための、柔軟で軽量な手法を提供します。

コアコンセプト:

  • リソース(Resources): すべてがリソース(ユーザー、製品、注文など)として扱われます。
  • 表現(Representations): リソースは複数の形式(JSON、XMLなど)で表現可能です。
  • ステートレス(Stateless): 各リクエストは処理に必要なすべての情報を保持します。
  • 統一インターフェース(Uniform Interface): リソースへのアクセスと操作方法が統一されています。

RESTful APIは、URLとして表現されるリソースに対して、HTTPリクエストを使用してCRUD(Create, Read, Update, Delete)操作を実行します。 RESTはステートレスです。つまり、クライアントからサーバーへの各リクエストは、そのリクエストを理解し処理するために必要なすべての情報を含んでいなければなりません。
SOAPやRPCとは異なり、RESTはプロトコルではなく、HTTP、URI、JSON、XMLといった既存のWeb標準を活用するアーキテクチャスタイルです。

2. RESTのコア原則

効果的なRESTful APIを設計するためには、以下の原則を理解することが不可欠です。これらにより、APIのスケーラビリティ、メンテナンス性、使いやすさが保証されます。

実践における主要原則:

  • リソースベース: アクション(動作)ではなく、リソース(モノ)に焦点を当てます。
  • ステートレス: 各リクエストは独立しており、自己完結しています。
  • キャッシュ可能: レスポンスがキャッシュ可能かどうかを定義します。
  • 階層化システム: クライアントは、背後のアーキテクチャ(プロキシやロードバランサなど)を意識する必要がありません。

RESTアーキテクチャの主要原則:

  1. クライアント・サーバー・アーキテクチャ: クライアントとサーバーの関心を分離します。
  2. ステートレス性: リクエスト間でクライアントのコンテキストをサーバー側に保存しません。
  3. キャッシュ可能性: レスポンスはそれ自体がキャッシュ可能か否かを明示する必要があります。
  4. 階層化システム: クライアントは最終的なサーバーに直接接続しているのか、中継器を通しているのか判別できません。
  5. 統一インターフェース: リクエストによるリソースの識別、表現を通じたリソースの操作、自己記述的なメッセージ、および HATEOAS(Hypertext As The Engine Of Application State)が含まれます。

3. HTTPメソッドとその用途

RESTful APIは、標準的なHTTPメソッドを使用してリソースに対する操作を実行します。

冪等性(Idempotency)と安全性:

  • 安全なメソッド: GET, HEAD, OPTIONS(リソースを変更しない)。
  • 冪等なメソッド: GET, PUT, DELETE(複数回同じリクエストを送っても、1回送ったのと同じ結果になる)。
  • 非冪等なメソッド: POST, PATCH(呼び出し回数によって結果が変わる可能性がある)。
メソッドアクション
GETリソースの取得GET /api/users
POST新規リソースの作成POST /api/users
PUTリソースの完全な更新PUT /api/users/123
PATCHリソースの部分的な更新PATCH /api/users/123
DELETEリソースの削除DELETE /api/users/123

3.1 HTTPメソッドの使用例

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

// JSONパース用のミドルウェア
app.use(express.json());

let users = [
  { id: 1, name: '田中 太郎', email: '[email protected]' },
  { id: 2, name: '佐藤 花子', email: '[email protected]' }
];

// GET - 全ユーザーの取得
app.get('/api/users', (req, res) => {
  res.json(users);
});

// GET - 特定ユーザーの取得
app.get('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ message: 'ユーザーが見つかりません' });
  res.json(user);
});

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

// PUT - ユーザーの完全更新
app.put('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ message: 'ユーザーが見つかりません' });

  user.name = req.body.name;
  user.email = req.body.email;

  res.json(user);
});

// DELETE - ユーザーの削除
app.delete('/api/users/:id', (req, res) => {
  const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
  if (userIndex === -1) return res.status(404).json({ message: 'ユーザーが見つかりません' });

  const deletedUser = users.splice(userIndex, 1);
  res.json(deletedUser[0]);
});

app.listen(8080, () => {
  console.log('REST APIサーバーがポート8080で起動しました');
});

4. RESTful APIの構造と設計

優れたAPI設計は、直感的で使いやすいパターンに従います。

設計時の考慮事項:

  • リソース命名: 動詞ではなく名詞を使用します(例: /getUsers ではなく /users)。
  • 複数形の使用: コレクションには複数形を使用します(例: /user/123 ではなく /users/123)。
  • 階層構造: リソースをネストさせて関係性を示します(例: /users/123/orders)。
  • フィルタリング/ソート: オプションの操作にはクエリパラメータを使用します。
  • バージョニング戦略: 最初からバージョニング(例: /v1/users)を計画します。

4.1 構造化されたAPIルートの例

// 適切なAPI構造
app.get('/api/products', getProducts);
app.get('/api/products/:id', getProductById);
app.get('/api/products/:id/reviews', getProductReviews);
app.get('/api/users/:userId/orders', getUserOrders);
app.post('/api/orders', createOrder);

// フィルタリングとページネーション
app.get('/api/products?category=electronics&sort=price&limit=10&page=2');

5. Node.jsとExpressによるREST APIの構築

Express.jsは、Node.jsでREST APIを構築するための優れた基盤を提供します。

5.1 推奨されるプロジェクト構造

- app.js            # メインアプリケーションファイル
- routes/           # ルート定義
  - users.js
  - products.js
- controllers/      # リクエストハンドラー(ロジック)
  - userController.js
- models/           # データモデル(DBアクセス)
  - User.js
- middleware/       # カスタムミドルウェア
  - auth.js
- config/           # 設定ファイル
  - db.js
- utils/            # ユーティリティ関数
  - errorHandler.js

6. Expressルーターの設定

// routes/users.js
const express = require('express');
const router = express.Router();
const { getUsers, getUserById, createUser, updateUser, deleteUser } = require('../controllers/userController');

router.get('/', getUsers);
router.get('/:id', getUserById);
router.post('/', createUser);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);

module.exports = router;

// app.js
const express = require('express');
const app = express();
const userRoutes = require('./routes/users');

app.use(express.json());
app.use('/api/users', userRoutes);

app.listen(8080, () => {
  console.log('サーバーがポート8080で実行中');
});

7. コントローラーの実装例

ルート、コントローラー、モデルに関心を分離することで、コードの保守性が向上します。

// controllers/userController.js
const User = require('../models/User');

const getUsers = async (req, res) => {
  try {
    const users = await User.findAll();
    res.status(200).json(users);
  } catch (error) {
    res.status(500).json({ message: 'ユーザー取得エラー', error: error.message });
  }
};

const getUserById = async (req, res) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) {
      return res.status(404).json({ message: 'ユーザーが見つかりません' });
    }
    res.status(200).json(user);
  } catch (error) {
    res.status(500).json({ message: 'ユーザー取得エラー', error: error.message });
  }
};

module.exports = { getUsers, getUserById };

8. APIのバージョン管理(Versioning)

URIパスによるバージョニングが最も一般的です。

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

// バージョン1のルート
const v1UserRoutes = require('./routes/v1/users');
app.use('/api/v1/users', v1UserRoutes);

// バージョン2のルート(新機能追加など)
const v2UserRoutes = require('./routes/v2/users');
app.use('/api/v2/users', v2UserRoutes);

app.listen(8080);

9. リクエストのバリデーション

データ整合性とセキュリティのために、リクエストの検証は必須です。Joi などのライブラリが役立ちます。

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

app.use(express.json());

// バリデーションスキーマの定義
const userSchema = Joi.object({
  name: Joi.string().min(3).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(18).max(120)
});

app.post('/api/users', (req, res) => {
  const { error } = userSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ message: error.details[0].message });
  }

  res.status(201).json({ message: 'ユーザーが正常に作成されました' });
});

app.listen(8080);

10. エラーハンドリングの集中管理

10.1 エラークラスとミドルウェア

// utils/errorHandler.js
class AppError extends Error {
  constructor(statusCode, message) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = true;

    Error.captureStackTrace(this, this.constructor);
  }
}

// middleware/errorMiddleware.js
const errorHandler = (err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  if (process.env.NODE_ENV === 'development') {
    res.status(err.statusCode).json({
      status: err.status,
      message: err.message,
      stack: err.stack,
      error: err
    });
  } else {
    // 本番環境用:詳細なエラー内容は秘匿する
    res.status(err.statusCode).json({
      status: err.status,
      message: err.isOperational ? err.message : '何かがうまくいきませんでした'
    });
  }
};

11. APIドキュメントの作成(Swagger)

Swagger/OpenAPI を使用すると、コードからドキュメントを自動生成できます。

const swaggerJsDoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const swaggerOptions = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'ユーザーAPI',
      version: '1.0.0',
      description: 'シンプルなExpressユーザーAPI'
    }
  },
  apis: ['./routes/*.js']
};

const swaggerDocs = swaggerJsDoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));

12. APIのテスト(Jest & Supertest)

// tests/users.test.js
const request = require('supertest');
const app = require('../app');

describe('ユーザーAPI', () => {
  it('すべてのユーザーを返すべきである', async () => {
    const res = await request(app).get('/api/users');
    expect(res.statusCode).toBe(200);
    expect(Array.isArray(res.body)).toBeTruthy();
  });

  it('新規ユーザーを作成すべきである', async () => {
    const userData = { name: 'テストユーザー', email: '[email protected]' };
    const res = await request(app).post('/api/users').send(userData);
    expect(res.statusCode).toBe(201);
    expect(res.body.name).toBe(userData.name);
  });
});

13. ベストプラクティスのまとめ

  • REST原則に従い、適切なHTTPメソッドを使用する。
  • エンドポイントには一貫した命名規則を使用する。
  • リソースベースのURLで論理的な構造にする。
  • レスポンスには適切なHTTPステータスコードを返す。
  • 明確なメッセージを伴うエラーハンドリングを実装する。
  • 大規模なデータセットにはページネーションを導入する。
  • 後方互換性を維持するためにAPIをバージョニングする。
  • セキュリティリスクを防ぐため、すべての入力をバリデーションする。
  • APIドキュメントを徹底的に作成する。
  • 信頼性を確保するため、包括的なテストを書く。
  • 本番環境では必ず HTTPS を使用する。
  • 濫用を防ぐために レート制限(Rate Limiting) を実装する。