NodeJS 速習チュートリアル

Node.js HTTP モジュール

1. 組み込みの HTTP モジュール

Node.js には、HTTP サーバーの作成や HTTP リクエストの送信を可能にする強力な組み込み HTTP モジュール が含まれています。
このモジュールは、Node.js で Web アプリケーションや API を構築する際に不可欠な要素です。

1.1 主要な機能

  • リクエストを処理し、レスポンスを送信する HTTP サーバーの作成
  • 他のサーバーへの HTTP リクエストの送信
  • 様々な HTTP メソッド(GET, POST, PUT, DELETE など)のハンドリング
  • リクエストヘッダーおよびレスポンスヘッダーの操作
  • 大容量ペイロードのためのストリーミングデータの処理

2. HTTP モジュールの読み込み

HTTP モジュールを使用するには、require() メソッドを使用してアプリケーションにインポートします。

// CommonJS の require を使用(Node.js のデフォルト)
const http = require('http');

// または ES modules を使用(Node.js 14 以降、package.json で "type": "module" を指定)
// import http from 'http';

3. HTTP サーバーの作成

HTTP モジュールの createServer() メソッドは、指定されたポートでリクエストを待機し、各リクエストに対してコールバック関数を実行する HTTP サーバーを作成します。

3.1 基本的な HTTP サーバーの例

// HTTP モジュールをインポート
const http = require('http');

// サーバーオブジェクトを作成
const server = http.createServer((req, res) => {
  // HTTP ステータスとコンテンツタイプを含むレスポンス HTTP ヘッダーを設定
  res.writeHead(200, { 'Content-Type': 'text/plain' });

  // レスポンスボディとして 'Hello, World!' を送信
  res.end('Hello, World!\n');
});

// リッスンするポートを定義
const PORT = 3000;

// サーバーを起動し、指定されたポートで待機
server.listen(PORT, 'localhost', () => {
  console.log(`Server running at http://localhost:${PORT}/`);
});

3.2 コードの解説

  • http.createServer():新しい HTTP サーバーインスタンスを作成します。
  • コールバック関数は各リクエストに対して実行され、2 つのパラメータを受け取ります:
    • req:リクエストオブジェクト(http.IncomingMessage
    • res:レスポンスオブジェクト(http.ServerResponse
  • res.writeHead():レスポンスのステータスコードとヘッダーを設定します。
  • res.end():レスポンスを送信し、接続を終了します。
  • server.listen():指定されたポートでサーバーを起動します。

3.3 サーバーの実行

1. コードを server.js という名前のファイルに保存します。

2. Node.js を使用してサーバーを実行します:

node server.js

3. ブラウザで http://localhost:3000 にアクセスしてレスポンスを確認します。

4. HTTP ヘッダーの操作

HTTP ヘッダーを使用すると、レスポンスとともに追加情報を送信できます。
res.writeHead() メソッドを使用して、ステータスコードとレスポンスヘッダーを設定します。

4.1 レスポンスヘッダーの設定例

const http = require('http');

const server = http.createServer((req, res) => {
  // ステータスコードと複数のヘッダーを設定
  res.writeHead(200, {
    'Content-Type': 'text/html',
    'X-Powered-By': 'Node.js',
    'Cache-Control': 'no-cache, no-store, must-revalidate',
    'Set-Cookie': 'sessionid=abc123; HttpOnly'
  });

  res.end('<h1>Hello, World!</h1>');
});

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

4.2 一般的な HTTP ステータスコード

コードメッセージ説明
200OK成功した HTTP リクエストに対する標準的なレスポンス
201Createdリクエストが完了し、新しいリソースが作成された
301Moved Permanentlyリソースが新しい URL に恒久的に移動した
400Bad Requestクライアント側のエラーにより、サーバーがリクエストを処理できない
401Unauthorized認証が必要
403Forbiddenサーバーがリクエストの認可を拒否した
404Not Foundリクエストされたリソースが見つからなかった
500Internal Server Errorサーバー内で予期しない条件に遭遇した

4.3 一般的なレスポンスヘッダー

  • Content-Type: コンテンツのメディアタイプを指定(例: text/html, application/json)
  • Content-Length: レスポンスボディの長さをバイト単位で指定
  • Location: リダイレクト(3xx ステータスコード)で使用
  • Set-Cookie: クライアントに HTTP クッキーを設定
  • Cache-Control: キャッシュメカニズムのディレクティブ
  • Access-Control-Allow-Origin: CORS(Cross-Origin Resource Sharing)サポート用

5. リクエストヘッダーの読み取り

req.headers オブジェクトを使用してリクエストヘッダーにアクセスできます。

const http = require('http');

const server = http.createServer((req, res) => {
  // すべてのリクエストヘッダーをログ出力
  console.log('Request Headers:', req.headers);

  // 特定のヘッダーを取得(大文字小文字を区別しない)
  const userAgent = req.headers['user-agent'];
  const acceptLanguage = req.headers['accept-language'];

  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end(`User-Agent: ${userAgent}\nAccept-Language: ${acceptLanguage}`);
});

server.listen(3000);

6. URL とクエリストリングの操作

Node.js は URL やクエリストリングを操作するための組み込みモジュールを提供しており、URL の各部分の処理やクエリパラメータのパースを容易にします。

6.1 リクエスト URL へのアクセス

req.url プロパティには、クエリパラメータを含むリクエストされた URL 文字列が含まれています。
これは http.IncomingMessage オブジェクトの一部です。

const http = require('http');

const server = http.createServer((req, res) => {
  // URL と HTTP メソッドを取得
  const { url, method } = req;

  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end(`You made a ${method} request to ${url}`);
});

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

6.2 URL モジュールを使用した URL のパース

url モジュールは、URL の解決とパースのためのユーティリティを提供します。
URL 文字列を、各パーツのプロパティを持つ URL オブジェクトにパースできます。

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
  // URL をパース
  const parsedUrl = url.parse(req.url, true);

  // URL の異なる部分を取得
  const pathname = parsedUrl.pathname; // クエリストリングを除いたパス
  const query = parsedUrl.query; // オブジェクトとしてのクエリストリング

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    pathname,
    query,
    fullUrl: req.url
  }, null, 2));
});

server.listen(3000);

リクエストとレスポンスの例
以下のリクエストに対して:
GET /products?category=electronics&sort=price&page=2 HTTP/1.1

サーバーは以下のようにレスポンスを返します:

{
  "pathname": "/products",
  "query": {
    "category": "electronics",
    "sort": "price",
    "page": "2"
  },
  "fullUrl": "/products?category=electronics&sort=price&page=2"
}

6.3 クエリストリングの処理

より高度なクエリストリングの処理には、querystring モジュールを使用できます。

const http = require('http');
const { URL } = require('url');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
  // 新しい URL API を使用(Node.js 10 以降推奨)
  const baseURL = 'http://' + req.headers.host + '/';
  const parsedUrl = new URL(req.url, baseURL);

  // クエリパラメータを取得
  const params = Object.fromEntries(parsedUrl.searchParams);

  // クエリストリングの構築例
  const queryObj = {
    name: 'John Doe',
    age: 30,
    interests: ['programming', 'music']
  };
  const queryStr = querystring.stringify(queryObj);

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    path: parsedUrl.pathname,
    params,
    exampleQueryString: queryStr
  }, null, 2));
});

server.listen(3000);

6.4 一般的な URL パースメソッド

  • url.parse(urlString, [parseQueryString], [slashesDenoteHost]): URL 文字列をオブジェクトにパースします。
  • url.format(urlObject): URL オブジェクトを URL 文字列にフォーマットします。
  • url.resolve(from, to): ベース URL に対してターゲット URL を解決します。
  • new URL(input, [base]): WHATWG URL API(新しいコードでの推奨)。
  • querystring.parse(str, [sep], [eq], [options]): クエリストリングをオブジェクトにパースします。
  • querystring.stringify(obj, [sep], [eq], [options]): オブジェクトをクエリストリングに変換します。

7. 異なる HTTP メソッドのハンドリング

RESTful API では通常、リソースに対して異なる操作を行うために、異なる HTTP メソッド(GET, POST, PUT, DELETE など)を使用します。

7.1 複数の HTTP メソッドを処理する例

const http = require('http');
const { URL } = require('url');

// インメモリのデータストア(デモ用)
let todos = [
  { id: 1, task: 'Learn Node.js', completed: false },
  { id: 2, task: 'Build an API', completed: false }
];

const server = http.createServer((req, res) => {
  const { method, url } = req;
  const parsedUrl = new URL(url, `http://${req.headers.host}`);
  const pathname = parsedUrl.pathname;

  // CORS ヘッダーの設定(開発用)
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

  // プリフライトリクエストのハンドリング
  if (method === 'OPTIONS') {
    res.writeHead(204);
    res.end();
    return;
  }

  // ルート: GET /todos
  if (method === 'GET' && pathname === '/todos') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(todos));
  }
  // ルート: POST /todos
  else if (method === 'POST' && pathname === '/todos') {
    let body = '';
    req.on('data', chunk => {
      body += chunk.toString();
    });

    req.on('end', () => {
      try {
        const newTodo = JSON.parse(body);
        newTodo.id = todos.length > 0 ? Math.max(...todos.map(t => t.id)) + 1 : 1;
        todos.push(newTodo);
        res.writeHead(201, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(newTodo));
      } catch (error) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Invalid JSON' }));
      }
    });
  }
  // ルート: PUT /todos/:id
  else if (method === 'PUT' && pathname.startsWith('/todos/')) {
    const id = parseInt(pathname.split('/')[2]);
    let body = '';

    req.on('data', chunk => {
      body += chunk.toString();
    });

    req.on('end', () => {
      try {
        const updatedTodo = JSON.parse(body);
        const index = todos.findIndex(t => t.id === id);

        if (index === -1) {
          res.writeHead(404, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({ error: 'Todo not found' }));
        } else {
          todos[index] = { ...todos[index], ...updatedTodo };
          res.writeHead(200, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify(todos[index]));
        }
      } catch (error) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Invalid JSON' }));
      }
    });
  }
  // ルート: DELETE /todos/:id
  else if (method === 'DELETE' && pathname.startsWith('/todos/')) {
    const id = parseInt(pathname.split('/')[2]);
    const index = todos.findIndex(t => t.id === id);

    if (index === -1) {
      res.writeHead(404, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Todo not found' }));
    } else {
      todos = todos.filter(t => t.id !== id);
      res.writeHead(204);
      res.end();
    }
  }
  // 404 Not Found
  else {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'Not Found' }));
  }
});

const PORT = 3000;
server.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}/`);
});

7.2 cURL を使用した API のテスト

以下の cURL コマンドを使用して API をテストできます:

1. すべての todo を取得

curl http://localhost:3000/todos

2. 新しい todo を作成

curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"task":"New Task","completed":false}'

2. todo を更新

curl -X PUT http://localhost:3000/todos/1 \
-H "Content-Type: application/json" \
-d '{"completed":true}'

4. todo を削除

curl -X DELETE http://localhost:3000/todos/1

7.3 HTTP メソッドのベストプラクティス

  • GET: リソースまたはリソースのコレクションを取得する(冪等であるべき)。
  • POST: 新しいリソースを作成する(冪等ではない)。
  • PUT: 既存のリソースを更新するか、存在しない場合は作成する(冪等)。
  • PATCH: リソースの一部を更新する。
  • DELETE: リソースを削除する(冪等)。
  • HEAD: GET と同じだが、レスポンスボディを含まない。
  • OPTIONS: ターゲットリソースの通信オプションを記述する。

8. エラーハンドリング

常に適切なエラーハンドリングと HTTP ステータスコードを含めるようにしましょう:

  • 200 OK: GET/PUT/PATCH の成功
  • 201 Created: リソース作成の成功
  • 204 No Content: DELETE の成功
  • 400 Bad Request: 無効なリクエストデータ
  • 401 Unauthorized: 認証が必要
  • 403 Forbidden: 権限不足
  • 404 Not Found: リソースが存在しない
  • 500 Internal Server Error: サーバー側のエラー

9. レスポンスのストリーミング

Node.js のストリームは、大量のデータを効率的に処理するために非常に強力です。HTTP モジュールは、リクエストボディの読み取りとレスポンスの書き込みの両方でストリームとうまく連携します。

9.1 大容量ファイルのストリーミング例

const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer((req, res) => {
  // URL からファイルパスを取得
  const filePath = path.join(__dirname, req.url);

  // ファイルが存在するか確認
  fs.access(filePath, fs.constants.F_OK, (err) => {
    if (err) {
      res.statusCode = 404;
      res.end('File not found');
      return;
    }

    // ファイル情報を取得
    fs.stat(filePath, (err, stats) => {
      if (err) {
        res.statusCode = 500;
        res.end('Server error');
        return;
      }

      // 適切なヘッダーを設定
      res.setHeader('Content-Length', stats.size);
      res.setHeader('Content-Type', 'application/octet-stream');

      // 読み取りストリームを作成し、レスポンスへパイプ(pipe)する
      const stream = fs.createReadStream(filePath);

      // エラーハンドリング
      stream.on('error', (err) => {
        console.error('Error reading file:', err);
        if (!res.headersSent) {
          res.statusCode = 500;
          res.end('Error reading file');
        }
      });

      // ファイルをレスポンスにパイプする
      stream.pipe(res);
    });
  });
});

const PORT = 3000;
server.listen(PORT, () => {
  console.log(`File server running at http://localhost:${PORT}/`);
});

9.2 ストリーミングのメリット

  • メモリ効率: データをすべてメモリにロードするのではなく、チャンク(分割されたデータ)単位で処理します。
  • レスポンスの高速化(TTFB): データが準備でき次第、すぐに送信を開始します。
  • バックプレッシャー(Backpressure)の制御: 読み取りストリームを一時停止することで、低速なクライアントを自動的に処理します。

9.3 ストリーミングの一般的なユースケース

  • ファイルのアップロード/ダウンロード
  • リアルタイムデータ処理
  • リクエストのプロキシ(中継)
  • ビデオ/オーディオストリーミング
  • ログの処理