404 Not Found

404 Not Found


nginx

「HTTPモジュール:ネイティブWebサーバーとREST APIの構築」

ボブはAPIのアイデアを素早く検証する必要があったが、Expressプロジェクトを丸ごとセットアップするのは避けたかったため、ネイティブのHTTPモジュールを使って、わずか20行のコードでAPIサーバーを立ち上げて稼働させた。リクエストメソッドの処理やURLパスの解析から、JSONデータの返却やステータスコードの設定に至るまで、ボブは、その基礎となる原理を理解すれば、フレームワークを使う方が実際にはずっと簡単になることに気づいた。

1. 学習内容


2. 初めてのHTTPサーバーの作成

http.createServer リクエストを受信するたびに呼び出されるコールバック関数を受け取ります。このコールバックには、request(リクエストオブジェクト)とresponse(レスポンスオブジェクト)の2つのパラメータが渡されます。server.listen はリスニングポートを指定します。

▶ 例:HTTPサーバーの最小化

JAVASCRIPT
const http = require('http');

const server = http.createServer((req, res) => {
  res.end('Hello, World!');
});

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

お使いのブラウザで http://localhost:3000/ にアクセスするだけで、Hello, World! を表示できます。


3. HTTPリクエスト・レスポンスのライフサイクル

すべてのHTTP通信は、「リクエスト → ルーティング → 処理 → レスポンス」というプロセスを経ます。このライフサイクルを理解することは、Webサービスを構築するための基礎となります。

100%
flowchart LR
    A["Client"] -->|"Send Request"| B["request Object<br/>method / url / headers"]
    B --> C["Routing Resolution<br/>pathname + searchParams"]
    C --> D{"Request Method?"}
    D -->|GET| E["Read Query Parameters"]
    D -->|POST / PUT| F["Collect the request body"]
    E --> G["Business Processing"]
    F --> G
    G --> H["response Object<br/>statusCode / headers / body"]
    H -->|"Return Response"| A

4. request オブジェクトの主要なプロパティ

request このオブジェクトには、クライアントから送信されたすべてのリクエスト情報が含まれています。最もよく使用される3つのプロパティは、methodurl、およびheadersです。

プロパティ/メソッド 説明 値の例
req.method 文字列 リクエストメソッド 'GET', 'POST'
req.url 文字列 リクエストパス(クエリ文字列を含む) '/api/users?id=1'
req.headers オブジェクト リクエストヘッダーオブジェクト { 'content-type': 'application/json' }
req.httpVersion 文字列 HTTPプロトコルのバージョン '1.1'
req.socket オブジェクト 基となるソケットオブジェクト

▶ 例:リクエスト情報の印刷

JAVASCRIPT
const http = require('http');

const server = http.createServer((req, res) => {
  console.log(`Method: ${req.method}`);
  console.log(`URL: ${req.url}`);
  console.log(`Content-Type: ${req.headers['content-type'] || 'N/A'}`);
  res.end('Check your terminal for request info.');
});

server.listen(3000);

curl を使用してテストリクエストを送信します:

BASH
curl -X POST http://localhost:3000/api/data -H "Content-Type: application/json"
TEXT
Method: POST
URL: /api/data
Content-Type: application/json

5. response オブジェクトの主要なメソッド

response このオブジェクトは、ステータスコード、レスポンスヘッダー、レスポンス本文などの応答データをクライアントに送信するために使用されます。

メソッド/プロパティ 説明
res.writeHead(statusCode, headers) 1回の操作でステータスコードと複数のレスポンスヘッダーを書き込む res.writeHead(200, { 'Content-Type': 'text/plain' })
res.statusCode = n ステータスコードを個別に設定 res.statusCode = 404
res.setHeader(name, value) 単一のレスポンスヘッダーを設定する res.setHeader('Content-Type', 'application/json')
res.write(data) レスポンス本文データを書き込み(複数回呼び出し可能) res.write('partial')
res.end(data) レスポンス本文を送信し、レスポンスを終了する res.end('done')
res.writeHead 呼び出しを受けた後、res.end() バッファ内のデータを送信

▶ 例:JSONレスポンスを返す

JAVASCRIPT
const http = require('http');

const server = http.createServer((req, res) => {
  const data = { message: 'Success', timestamp: Date.now() };

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
});

server.listen(3000);
BASH
curl http://localhost:3000/
TEXT
{"message":"Success","timestamp":1719792000000}

6. URLルーティングの解決

req.url には、リクエストのパスとクエリ文字列がすべて含まれています。new URL() を使用すると、パス名と検索パラメータを簡単に分割でき、パスベースのルーティングが可能になります。

▶ 例:パスベースのルーティング

JAVASCRIPT
const http = require('http');

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

  res.writeHead(200, { 'Content-Type': 'text/plain' });

  if (pathname === '/') {
    res.end('Home Page');
  } else if (pathname === '/about') {
    res.end('About Page');
  } else if (pathname === '/api/status') {
    res.end('OK');
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
});

server.listen(3000);

7. GETリクエストとクエリパラメータ

GETリクエストのパラメータはURLのクエリ文字列に含まれており、url.searchParams を使用することで、キーと値のペアを直接取得することができます。

▶ 例:クエリパラメータの解析

JAVASCRIPT
const http = require('http');

const server = http.createServer((req, res) => {
  if (req.method !== 'GET') {
    res.writeHead(405, { 'Content-Type': 'text/plain' });
    res.end('Method Not Allowed');
    return;
  }

  const url = new URL(req.url, `http://${req.headers.host}`);
  const name = url.searchParams.get('name') || 'Guest';
  const page = url.searchParams.get('page') || '1';

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ name, page }));
});

server.listen(3000);
BASH
curl "http://localhost:3000/?name=Bob&page=3"
TEXT
{"name":"Bob","page":"3"}

注:searchParams.get() は常に文字列を返すため、手動で数値やその他の型に変換する必要があります。


8. POSTリクエストとリクエスト本文の収集

POSTリクエストのデータは、リクエスト本文を介して送信されます。request オブジェクトは読み取り可能なストリームです。データブロックを取得するには data イベントを監視し、データ全体を処理するには end イベントを監視する必要があります。

▶ 例:POSTリクエストのボディを取得する

JAVASCRIPT
const http = require('http');

const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.url === '/api/users') {
    let body = '';

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

    req.on('end', () => {
      try {
        const data = JSON.parse(body);
        res.writeHead(201, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ id: 1, ...data }));
      } catch (e) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Invalid JSON' }));
      }
    });
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
});

server.listen(3000);
BASH
curl -X POST http://localhost:3000/api/users -H "Content-Type: application/json" -d "{\"name\":\"Bob\",\"age\":30}"
TEXT
{"id":1,"name":"Bob","age":30}

9. Content-Type とレスポンスヘッダー

Content-Type これはHTTP通信において最も重要なレスポンスヘッダーの一つであり、クライアントに対するレスポンス本文のデータ形式を指定するものです。これを誤って設定すると、クライアントがデータを正しく解析できなくなります。

コンテンツタイプ 目的 説明
text/plain プレーンテキスト 書式設定のない、最も基本的なテキスト形式
text/html HTMLページ ブラウザはこれをウェブページとして表示します
application/json JSONデータ APIで最も一般的に使用されるレスポンス形式
application/x-www-form-urlencoded フォームデータ デフォルトのフォーム送信形式
multipart/form-data ファイルのアップロード ファイルを添付したフォームの送信
application/xml XMLデータ SOAP API またはレガシーインターフェース
text/css CSSスタイルシート スタイルシート
application/octet-stream バイナリストリーム ファイルダウンロードのシナリオ

▶ 例:同じデータに対する異なるコンテンツタイプの影響

JAVASCRIPT
const http = require('http');

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

  if (url.pathname === '/plain') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('<h1>This is plain text</h1>');
  } else if (url.pathname === '/html') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<h1>This is HTML</h1>');
  } else if (url.pathname === '/json') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'This is JSON' }));
  }
});

server.listen(3000);

/plain にアクセスすると、ブラウザにはソースコードタブが表示されます。/html にアクセスすると、ブラウザにはメインの見出しが表示されます。/json にアクセスすると、ブラウザには JSON データが表示されます。


10. HTTPステータスコードのクイックリファレンス

ステータスコードとは、リクエストに対するサーバーの応答を標準化された形式で表したものであり、クライアントはこのステータスコードに基づいて次の動作を決定します。

ステータスコード カテゴリ 意味 よくあるケース
200 2xx 成功 OK GETリクエストによりデータが正常に返されました
201 2xx 成功 作成済み POST リソースが正常に作成されました
204 2xx 成功 コンテンツなし 削除に成功、コンテンツは返されませんでした
301 3xx リダイレクト 永久移動 新しい URL への永久リダイレクト
302 3xx リダイレクト 見つかりました 一時的なリダイレクト
304 3xx リダイレクト 変更なし キャッシュヒット;再送信不要
400 4xx クライアントエラー 不正なリクエスト リクエストパラメータの形式が無効です
401 4xx クライアントエラー アクセス拒否 認証されていません。ログインが必要です
403 4xx クライアントエラー アクセス拒否 認証済みだが権限なし
404 4xx クライアントエラー 見つかりません ルートまたはリソースが存在しません
405 4xx クライアントエラー 許可されていないメソッド 指定されたリクエストメソッドは許可されていません
500 5xx サーバーエラー 内部サーバーエラー 内部サーバーエラー
502 5xx サーバーエラー ゲートウェイエラー ゲートウェイ/プロキシが不正な応答を受信しました
503 5xx サーバーエラー サービス利用不可 サービスが一時的に利用できません

▶ 例:条件に応じて異なるステータスコードを返す

JAVASCRIPT
const http = require('http');

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);
  const id = url.searchParams.get('id');

  if (!id) {
    res.writeHead(400, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'Missing id parameter' }));
  } else if (id === '0') {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'User not found' }));
  } else {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ id, name: 'Bob' }));
  }
});

server.listen(3000);

11. 総合的な例:シンプルなREST APIサーバー

これまでに学んだすべての概念を組み合わせて、GET/POSTルート、JSONレスポンス、およびクエリパラメータの解析に対応したREST APIサーバーを構築します。ユーザーの一覧をメモリ内に保持し、すべてのユーザーの照会、特定のユーザーの照会、およびユーザーの作成という3つの操作に対応させます。

JAVASCRIPT
const http = require('http');

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

function parseBody(req) {
  return new Promise((resolve, reject) => {
    let body = '';
    req.on('data', (chunk) => { body += chunk.toString(); });
    req.on('end', () => {
      try { resolve(JSON.parse(body)); }
      catch (e) { reject(e); }
    });
    req.on('error', reject);
  });
}

function sendJSON(res, statusCode, data) {
  res.writeHead(statusCode, {
    'Content-Type': 'application/json',
    'X-Powered-By': 'Node.js',
  });
  res.end(JSON.stringify(data));
}

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

  // GET /api/users
  if (req.method === 'GET' && pathname === '/api/users') {
    const limit = parseInt(url.searchParams.get('limit')) || 10;
    sendJSON(res, 200, users.slice(0, limit));
    return;
  }

  // GET /api/users/:id
  if (req.method === 'GET' && pathname.startsWith('/api/users/')) {
    const id = parseInt(pathname.split('/').pop());
    const user = users.find((u) => u.id === id);
    if (!user) {
      sendJSON(res, 404, { error: 'User not found' });
    } else {
      sendJSON(res, 200, user);
    }
    return;
  }

  // POST /api/users
  if (req.method === 'POST' && pathname === '/api/users') {
    try {
      const data = await parseBody(req);
      if (!data.name || !data.email) {
        sendJSON(res, 400, { error: 'name and email are required' });
        return;
      }
      const newUser = { id: nextId++, name: data.name, email: data.email };
      users.push(newUser);
      sendJSON(res, 201, newUser);
    } catch (e) {
      sendJSON(res, 400, { error: 'Invalid JSON body' });
    }
    return;
  }

  // 404 fallback
  sendJSON(res, 404, { error: 'Route not found' });
});

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

すべてのインターフェースをテストする:

BASH
# Query All Users
curl http://localhost:3000/api/users

# Query a Single User
curl http://localhost:3000/api/users/1

# Create a New User
curl -X POST http://localhost:3000/api/users -H "Content-Type: application/json" -d "{\"name\":\"Charlie\",\"email\":\"charlie@example.com\"}"

# Accessing a Route That Does Not Exist
curl http://localhost:3000/unknown
TEXT
[{"id":1,"name":"Bob","email":"bob@example.com"},{"id":2,"name":"Alice","email":"alice@example.com"}]

{"id":1,"name":"Bob","email":"bob@example.com"}

{"id":3,"name":"Charlie","email":"charlie@example.com"}

{"error":"Route not found"}

❓ よくある質問

Q httpモジュールとExpressの違いは何ですか?
A httpはNode.jsに組み込まれている低レベルのモジュールで、最も基本的なリクエスト/レスポンス処理のみを提供します。一方、Expressはhttpモジュールの上に構築されたフレームワークであり、ルーティング、ミドルウェア、テンプレートエンジンなどの高度な機能を備えており、開発効率を高める一方で、追加の依存関係が生じます。
Q POSTリクエストのリクエスト本文を取得するにはどうすればよいですか?
A req オブジェクトは読み取り可能なストリームです。data イベントを監視してバッファブロックを収集し、それらを連結した後、データ受信完了を示す end イベントを監視し、最後に JSON.parse() または Buffer.concat() を使用して完全なデータを処理する必要があります。
Q res.end() と res.write() の違いは何ですか?
A res.end() はデータを送信し、レスポンスを閉じます。これは各リクエストに対して一度だけ、かつ一度のみ呼び出す必要があります。res.write() データを書き込むだけで、レスポンスを閉じません。ストリーミングやチャンク転送のために複数回呼び出すことはできますが、res.end() 最後にレスポンスを閉じるために必ず呼び出す必要があります。
Q JSONを返す際にContent-Typeを設定する必要があるのはなぜですか?
A Content-Typeが設定されていない場合、デフォルトはtext/plainになります。クライアント(ブラウザ、fetch、curl)はレスポンスを自動的にJSONとして解析しないため、response.json()エラーが発生したり、データが正しく表示されなかったりする可能性があります。application/jsonに設定することで、クライアントがレスポンス本文を正しく解析できるようになります。
Q URL内のクエリパラメータはどのように処理すればよいですか?
A new URL(req.url, 'http://localhost') を使用して URL オブジェクトを作成し、url.searchParams.get('key') でパラメータの値を取得するか、url.searchParams.entries() を使用してすべてのパラメータを順に処理します。
Q server.listen の 2 番目のコールバックの目的は何ですか?
A これは、サーバーの起動に成功した後に発火するコールバックであり、起動ログを出力するためによく使用されます。コールバックを渡さない場合は、server.on('listening', callback) イベントをリッスンすることもでき、これでも同じ効果が得られます。
Q new URL の 2 番目のパラメータに base を含める必要があるのはなぜですか?
A req.url にはパス部分のみ(例:/api?id=1)が含まれており、完全な URL ではありません。new URL()は、プロトコルとホスト名を補完するためにbaseパラメータを必要とします。そうでない場合、TypeErrorがスローされます。baseの値は、pathnameおよびsearchParamsの解析結果に影響を与えません。

📖 まとめ


📝 練習問題

  1. このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
  2. 包括的な例を修正し、独自の拡張機能を追加する
  3. 公式ドキュメントを確認し、このレッスンで取り上げられていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
  4. 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
  5. このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%