「HTTPモジュール:ネイティブWebサーバーとREST APIの構築」
ボブはAPIのアイデアを素早く検証する必要があったが、Expressプロジェクトを丸ごとセットアップするのは避けたかったため、ネイティブのHTTPモジュールを使って、わずか20行のコードでAPIサーバーを立ち上げて稼働させた。リクエストメソッドの処理やURLパスの解析から、JSONデータの返却やステータスコードの設定に至るまで、ボブは、その基礎となる原理を理解すれば、フレームワークを使う方が実際にはずっと簡単になることに気づいた。
1. 学習内容
http.createServer/server.listenを使用して HTTP サーバーを作成するrequestオブジェクトの主要なプロパティ(メソッド/URL/ヘッダー)を読み取るresponseオブジェクトを使用して応答(writeHead / end / statusCode)を送信するnew URL()を使用して、ルートパスとクエリパラメータを解析する- GETリクエストとクエリパラメータの処理
- POSTリクエストの処理とリクエスト本文の取得
- Content-Type およびカスタム応答ヘッダーを設定する
- 一般的なHTTPステータスコードの意味と使用例
2. 初めてのHTTPサーバーの作成
http.createServer リクエストを受信するたびに呼び出されるコールバック関数を受け取ります。このコールバックには、request(リクエストオブジェクト)とresponse(レスポンスオブジェクト)の2つのパラメータが渡されます。server.listen はリスニングポートを指定します。
▶ 例:HTTPサーバーの最小化
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/');
});
node server.js
Server running at http://localhost:3000/
お使いのブラウザで http://localhost:3000/ にアクセスするだけで、Hello, World! を表示できます。
3. HTTPリクエスト・レスポンスのライフサイクル
すべてのHTTP通信は、「リクエスト → ルーティング → 処理 → レスポンス」というプロセスを経ます。このライフサイクルを理解することは、Webサービスを構築するための基礎となります。
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つのプロパティは、method、url、およびheadersです。
| プロパティ/メソッド | 型 | 説明 | 値の例 |
|---|---|---|---|
req.method |
文字列 | リクエストメソッド | 'GET', 'POST' |
req.url |
文字列 | リクエストパス(クエリ文字列を含む) | '/api/users?id=1' |
req.headers |
オブジェクト | リクエストヘッダーオブジェクト | { 'content-type': 'application/json' } |
req.httpVersion |
文字列 | HTTPプロトコルのバージョン | '1.1' |
req.socket |
オブジェクト | 基となるソケットオブジェクト | — |
▶ 例:リクエスト情報の印刷
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 を使用してテストリクエストを送信します:
curl -X POST http://localhost:3000/api/data -H "Content-Type: application/json"
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レスポンスを返す
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);
curl http://localhost:3000/
{"message":"Success","timestamp":1719792000000}
6. URLルーティングの解決
req.url には、リクエストのパスとクエリ文字列がすべて含まれています。new URL() を使用すると、パス名と検索パラメータを簡単に分割でき、パスベースのルーティングが可能になります。
▶ 例:パスベースのルーティング
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 を使用することで、キーと値のペアを直接取得することができます。
▶ 例:クエリパラメータの解析
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);
curl "http://localhost:3000/?name=Bob&page=3"
{"name":"Bob","page":"3"}
注:
searchParams.get()は常に文字列を返すため、手動で数値やその他の型に変換する必要があります。
8. POSTリクエストとリクエスト本文の収集
POSTリクエストのデータは、リクエスト本文を介して送信されます。request オブジェクトは読み取り可能なストリームです。データブロックを取得するには data イベントを監視し、データ全体を処理するには end イベントを監視する必要があります。
▶ 例:POSTリクエストのボディを取得する
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);
curl -X POST http://localhost:3000/api/users -H "Content-Type: application/json" -d "{\"name\":\"Bob\",\"age\":30}"
{"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 |
バイナリストリーム | ファイルダウンロードのシナリオ |
▶ 例:同じデータに対する異なるコンテンツタイプの影響
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 サーバーエラー | サービス利用不可 | サービスが一時的に利用できません |
▶ 例:条件に応じて異なるステータスコードを返す
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つの操作に対応させます。
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/');
});
すべてのインターフェースをテストする:
# 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
[{"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"}
❓ よくある質問
req オブジェクトは読み取り可能なストリームです。data イベントを監視してバッファブロックを収集し、それらを連結した後、データ受信完了を示す end イベントを監視し、最後に JSON.parse() または Buffer.concat() を使用して完全なデータを処理する必要があります。res.end() はデータを送信し、レスポンスを閉じます。これは各リクエストに対して一度だけ、かつ一度のみ呼び出す必要があります。res.write() データを書き込むだけで、レスポンスを閉じません。ストリーミングやチャンク転送のために複数回呼び出すことはできますが、res.end() 最後にレスポンスを閉じるために必ず呼び出す必要があります。text/plainになります。クライアント(ブラウザ、fetch、curl)はレスポンスを自動的にJSONとして解析しないため、response.json()エラーが発生したり、データが正しく表示されなかったりする可能性があります。application/jsonに設定することで、クライアントがレスポンス本文を正しく解析できるようになります。new URL(req.url, 'http://localhost') を使用して URL オブジェクトを作成し、url.searchParams.get('key') でパラメータの値を取得するか、url.searchParams.entries() を使用してすべてのパラメータを順に処理します。server.listen の 2 番目のコールバックの目的は何ですか?server.on('listening', callback) イベントをリッスンすることもでき、これでも同じ効果が得られます。new URL の 2 番目のパラメータに base を含める必要があるのはなぜですか?req.url にはパス部分のみ(例:/api?id=1)が含まれており、完全な URL ではありません。new URL()は、プロトコルとホスト名を補完するためにbaseパラメータを必要とします。そうでない場合、TypeErrorがスローされます。baseの値は、pathnameおよびsearchParamsの解析結果に影響を与えません。📖 まとめ
- 重要な概念とその活用方法
- 基本概念と最初のHTTPサーバーの使い方
- HTTPリクエスト・レスポンスのライフサイクルの基本概念と利用方法
requestオブジェクトの中核となるプロパティの主要な概念と使用方法responseオブジェクトの中核となるメソッドの主要な概念と使用方法- URLルーティングの基本概念と使い方
- GETリクエストとクエリパラメータの基本概念と使い方
- POSTリクエストの基本概念と使用方法、およびリクエストボディの収集
📝 練習問題
- このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
- 包括的な例を修正し、独自の拡張機能を追加する
- 公式ドキュメントを確認し、このレッスンで取り上げられていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
- 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
- このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。



