Node.js HTTP サーバーをゼロから作る実践ガイド

Node.js を使って Web アプリケーションを開発する際に、必ず理解しておきたいのが HTTP サーバーの仕組みです。フレームワークを使わずに、組み込みモジュールだけで HTTP サーバーを構築することで、Web 開発の根幹となる技術を深く理解できるでしょう。
この記事では、Node.js の http
モジュールを使って、ゼロから実用的な HTTP サーバーを段階的に構築していきます。初心者の方でも安心して学習できるよう、基本概念から始めて、実際に手を動かしながら機能を追加していく構成になっています。
HTTP サーバーとは何か
Web 通信の基本的な仕組み
HTTP(HyperText Transfer Protocol)サーバーは、クライアント(主に Web ブラウザ)からのリクエストを受け取り、適切なレスポンスを返すプログラムです。
項目 | 説明 | 具体例 |
---|---|---|
クライアント | リクエストを送信する側 | Web ブラウザ、モバイルアプリ、API クライアント |
サーバー | リクエストを処理してレスポンスを返す側 | Node.js アプリケーション、Apache、Nginx |
HTTP メソッド | 操作の種類を示す | GET(取得)、POST(作成)、PUT(更新)、DELETE(削除) |
ステータスコード | 処理結果を示す数値 | 200(成功)、404(見つからない)、500(サーバーエラー) |
HTTP 通信の流れ
HTTP 通信は、リクエスト・レスポンスモデルで動作します。
javascript// HTTP通信の基本的な流れ
/*
1. クライアント → サーバー: HTTPリクエスト送信
GET /users HTTP/1.1
Host: example.com
2. サーバー内部: リクエスト処理
- URLの解析
- 必要なデータの取得
- レスポンス作成
3. サーバー → クライアント: HTTPレスポンス送信
HTTP/1.1 200 OK
Content-Type: application/json
{"users": [...]}
*/
Node.js 組み込み http モジュールの基礎
http モジュールの特徴
Node.js の http
モジュールは、HTTP サーバーとクライアントの両方の機能を提供する組み込みモジュールです。外部ライブラリを使わずに、本格的な Web サーバーを構築できる強力な機能を持っています。
javascript// httpモジュールの基本的なインポート
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
// httpモジュールの主要なメソッド
// - http.createServer(): HTTPサーバーを作成
// - server.listen(): 指定ポートでサーバーを起動
// - request: クライアントからのリクエスト情報
// - response: クライアントへのレスポンス送信機能
リクエストとレスポンスオブジェクト
HTTP サーバーでは、リクエストとレスポンスの 2 つのオブジェクトが中心的な役割を果たします。
javascript// リクエストオブジェクトの主要プロパティ
const examineRequest = (request) => {
console.log('HTTPメソッド:', request.method);
console.log('URL:', request.url);
console.log('ヘッダー:', request.headers);
console.log('User-Agent:', request.headers['user-agent']);
};
// レスポンスオブジェクトの主要メソッド
const sendResponse = (response) => {
// ステータスコードとヘッダーの設定
response.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
});
// レスポンスボディの送信
response.write('<h1>こんにちは、Node.js!</h1>');
response.end(); // レスポンス送信完了
};
最小構成の HTTP サーバー作成
Hello World サーバーの実装
まずは、最もシンプルな HTTP サーバーを作成してみましょう。
javascript// server-basic.js - 最小構成のHTTPサーバー
const http = require('http');
// サーバーの作成
const server = http.createServer((request, response) => {
// レスポンスヘッダーの設定
response.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
});
// HTMLレスポンスの送信
response.end(`
<!DOCTYPE html>
<html>
<head>
<title>Node.js HTTPサーバー</title>
<meta charset="utf-8">
</head>
<body>
<h1>Hello, Node.js HTTP Server!</h1>
<p>サーバーが正常に動作しています。</p>
<p>現在時刻: ${new Date().toLocaleString('ja-JP')}</p>
</body>
</html>
`);
});
// サーバーの起動
const PORT = 3000;
server.listen(PORT, () => {
console.log(
`サーバーが起動しました: http://localhost:${PORT}`
);
});
// エラーハンドリング
server.on('error', (error) => {
console.error('サーバーエラー:', error.message);
});
このコードを実行すると、ブラウザで http://localhost:3000
にアクセスできる Web サーバーが起動します。
サーバー起動と動作確認
bash# サーバーの起動
node server-basic.js
# ブラウザでアクセス
# http://localhost:3000
# またはcurlコマンドでテスト
curl http://localhost:3000
リクエスト・レスポンス処理の実装
リクエスト情報の詳細取得
実用的なサーバーでは、クライアントからのリクエスト情報を詳しく解析する必要があります。
javascript// server-request-analysis.js - リクエスト解析サーバー
const http = require('http');
const url = require('url');
const server = http.createServer((request, response) => {
// URLの解析
const parsedUrl = url.parse(request.url, true);
const pathname = parsedUrl.pathname;
const query = parsedUrl.query;
// リクエスト情報の収集
const requestInfo = {
method: request.method,
url: request.url,
pathname: pathname,
query: query,
headers: request.headers,
userAgent: request.headers['user-agent'],
timestamp: new Date().toISOString(),
};
// コンソールにリクエスト情報を出力
console.log('=== 新しいリクエスト ===');
console.log(
`${requestInfo.method} ${requestInfo.pathname}`
);
console.log('クエリパラメータ:', requestInfo.query);
console.log('User-Agent:', requestInfo.userAgent);
// レスポンスの送信
response.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8',
});
response.end(JSON.stringify(requestInfo, null, 2));
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(
`リクエスト解析サーバー起動: http://localhost:${PORT}`
);
console.log(
'例: http://localhost:3000/test?name=山田&age=25'
);
});
POST データの受信処理
POST リクエストのボディデータを受信する処理を実装してみましょう。
javascript// server-post-handler.js - POSTデータ処理サーバー
const http = require('http');
const url = require('url');
const server = http.createServer((request, response) => {
const parsedUrl = url.parse(request.url, true);
const pathname = parsedUrl.pathname;
// CORSヘッダーの設定(開発用)
response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE'
);
response.setHeader(
'Access-Control-Allow-Headers',
'Content-Type'
);
if (request.method === 'POST' && pathname === '/submit') {
let body = '';
// データの受信
request.on('data', (chunk) => {
body += chunk.toString();
});
// データ受信完了
request.on('end', () => {
try {
// JSON形式のデータをパース
const postData = JSON.parse(body);
// 受信データの処理
const processedData = {
received: postData,
processed_at: new Date().toISOString(),
server_message: `${
postData.name || 'Anonymous'
}さん、データを受信しました!`,
};
console.log('POST データ受信:', postData);
// 成功レスポンス
response.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8',
});
response.end(
JSON.stringify(processedData, null, 2)
);
} catch (error) {
// JSONパースエラー
response.writeHead(400, {
'Content-Type': 'application/json; charset=utf-8',
});
response.end(
JSON.stringify({
error: 'Invalid JSON format',
message: error.message,
})
);
}
});
} else {
// その他のリクエストに対する簡単なレスポンス
response.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
});
response.end(`
<h1>POST データテストサーバー</h1>
<p>POST /submit エンドポイントでJSONデータを受信できます。</p>
<pre>
例:
curl -X POST http://localhost:3000/submit \\
-H "Content-Type: application/json" \\
-d '{"name": "山田太郎", "email": "yamada@example.com"}'
</pre>
`);
}
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(
`POST処理サーバー起動: http://localhost:${PORT}`
);
});
ルーティング機能の追加
基本的なルーティングシステム
複数のエンドポイントを処理できるルーティングシステムを実装します。
javascript// server-routing.js - ルーティング機能付きサーバー
const http = require('http');
const url = require('url');
class SimpleRouter {
constructor() {
this.routes = {
GET: new Map(),
POST: new Map(),
PUT: new Map(),
DELETE: new Map(),
};
}
// ルートの登録
get(path, handler) {
this.routes.GET.set(path, handler);
}
post(path, handler) {
this.routes.POST.set(path, handler);
}
put(path, handler) {
this.routes.PUT.set(path, handler);
}
delete(path, handler) {
this.routes.DELETE.set(path, handler);
}
// リクエストの処理
handle(request, response) {
const parsedUrl = url.parse(request.url, true);
const pathname = parsedUrl.pathname;
const method = request.method;
// 対応するハンドラーを検索
const handler = this.routes[method]?.get(pathname);
if (handler) {
// ルートが見つかった場合
handler(request, response, parsedUrl.query);
} else {
// 404エラー
this.send404(response, pathname);
}
}
// 404エラーレスポンス
send404(response, pathname) {
response.writeHead(404, {
'Content-Type': 'application/json; charset=utf-8',
});
response.end(
JSON.stringify({
error: 'Not Found',
message: `パス '${pathname}' は見つかりませんでした`,
timestamp: new Date().toISOString(),
})
);
}
}
// ルーターのインスタンス作成
const router = new SimpleRouter();
// ルートの定義
router.get('/', (request, response, query) => {
response.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
});
response.end(`
<h1>Node.js ルーティングサーバー</h1>
<h2>利用可能なエンドポイント:</h2>
<ul>
<li><a href="/users">GET /users - ユーザー一覧</a></li>
<li><a href="/users/123">GET /users/123 - 特定ユーザー</a></li>
<li><a href="/api/health">GET /api/health - ヘルスチェック</a></li>
<li>POST /api/users - ユーザー作成</li>
</ul>
`);
});
router.get('/users', (request, response, query) => {
const users = [
{
id: 1,
name: '山田太郎',
email: 'yamada@example.com',
},
{ id: 2, name: '佐藤花子', email: 'sato@example.com' },
{
id: 3,
name: '田中一郎',
email: 'tanaka@example.com',
},
];
response.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8',
});
response.end(
JSON.stringify(
{
users: users,
total: users.length,
query: query,
},
null,
2
)
);
});
router.get('/api/health', (request, response, query) => {
response.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8',
});
response.end(
JSON.stringify({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
})
);
});
// POSTルートの例
router.post('/api/users', (request, response, query) => {
let body = '';
request.on('data', (chunk) => {
body += chunk.toString();
});
request.on('end', () => {
try {
const userData = JSON.parse(body);
// 新しいユーザーの作成(模擬)
const newUser = {
id: Date.now(),
...userData,
created_at: new Date().toISOString(),
};
response.writeHead(201, {
'Content-Type': 'application/json; charset=utf-8',
});
response.end(
JSON.stringify({
message: 'ユーザーが作成されました',
user: newUser,
})
);
} catch (error) {
response.writeHead(400, {
'Content-Type': 'application/json; charset=utf-8',
});
response.end(
JSON.stringify({
error: 'Invalid JSON',
message: error.message,
})
);
}
});
});
// サーバーの作成と起動
const server = http.createServer((request, response) => {
router.handle(request, response);
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(
`ルーティングサーバー起動: http://localhost:${PORT}`
);
});
静的ファイルの配信
ファイルシステムを使った静的ファイル配信
HTML、CSS、JavaScript、画像ファイルなどの静的ファイルを配信する機能を実装します。
javascript// server-static.js - 静的ファイル配信サーバー
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
class StaticFileServer {
constructor(rootDirectory = './public') {
this.rootDirectory = path.resolve(rootDirectory);
// MIMEタイプの定義
this.mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.txt': 'text/plain; charset=utf-8',
};
}
// MIMEタイプの取得
getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
return (
this.mimeTypes[ext] || 'application/octet-stream'
);
}
// 静的ファイルの配信
serveStaticFile(request, response) {
const parsedUrl = url.parse(request.url);
let pathname = parsedUrl.pathname;
// デフォルトファイルの設定
if (pathname === '/') {
pathname = '/index.html';
}
// ファイルパスの構築
const filePath = path.join(
this.rootDirectory,
pathname
);
// セキュリティチェック(ディレクトリトラバーサル対策)
if (!filePath.startsWith(this.rootDirectory)) {
this.sendError(response, 403, 'Forbidden');
return;
}
// ファイルの存在確認と読み込み
fs.readFile(filePath, (error, data) => {
if (error) {
if (error.code === 'ENOENT') {
// ファイルが見つからない
this.sendError(response, 404, 'File Not Found');
} else {
// その他のエラー
this.sendError(
response,
500,
'Internal Server Error'
);
}
return;
}
// 成功時のレスポンス
const mimeType = this.getMimeType(filePath);
response.writeHead(200, {
'Content-Type': mimeType,
'Content-Length': data.length,
});
response.end(data);
console.log(
`静的ファイル配信: ${pathname} (${mimeType})`
);
});
}
// エラーレスポンスの送信
sendError(response, statusCode, message) {
response.writeHead(statusCode, {
'Content-Type': 'text/html; charset=utf-8',
});
response.end(`
<!DOCTYPE html>
<html>
<head>
<title>Error ${statusCode}</title>
<meta charset="utf-8">
</head>
<body>
<h1>Error ${statusCode}</h1>
<p>${message}</p>
<hr>
<p>Node.js Static File Server</p>
</body>
</html>
`);
}
}
// 静的ファイルサーバーのインスタンス作成
const staticServer = new StaticFileServer('./public');
// サーバーの作成
const server = http.createServer((request, response) => {
staticServer.serveStaticFile(request, response);
});
// publicディレクトリの作成とサンプルファイルの生成
const publicDir = './public';
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
// サンプルHTMLファイルの作成
const sampleHTML = `
<!DOCTYPE html>
<html>
<head>
<title>Node.js 静的ファイルサーバー</title>
<meta charset="utf-8">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>静的ファイル配信テスト</h1>
<p>このページは Node.js の HTTP サーバーから配信されています。</p>
<script src="/script.js"></script>
</body>
</html>
`.trim();
const sampleCSS = `
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
`.trim();
const sampleJS = `
console.log('Node.js 静的ファイルサーバーから JavaScript が読み込まれました!');
document.addEventListener('DOMContentLoaded', () => {
const h1 = document.querySelector('h1');
h1.style.color = '#007bff';
});
`.trim();
fs.writeFileSync(
path.join(publicDir, 'index.html'),
sampleHTML
);
fs.writeFileSync(
path.join(publicDir, 'style.css'),
sampleCSS
);
fs.writeFileSync(
path.join(publicDir, 'script.js'),
sampleJS
);
console.log('サンプルファイルを作成しました: ./public/');
}
const PORT = 3000;
server.listen(PORT, () => {
console.log(
`静的ファイルサーバー起動: http://localhost:${PORT}`
);
console.log(
`公開ディレクトリ: ${staticServer.rootDirectory}`
);
});
JSON データの送受信
REST API の実装
JSON データを使った REST API を実装して、データの CRUD 操作を行えるサーバーを作成します。
javascript// server-json-api.js - JSON API サーバー
const http = require('http');
const url = require('url');
class JSONAPIServer {
constructor() {
// メモリ上のデータストア(本番環境ではデータベースを使用)
this.users = [
{
id: 1,
name: '山田太郎',
email: 'yamada@example.com',
age: 30,
},
{
id: 2,
name: '佐藤花子',
email: 'sato@example.com',
age: 25,
},
{
id: 3,
name: '田中一郎',
email: 'tanaka@example.com',
age: 35,
},
];
this.nextId = 4;
}
// リクエストボディの取得(Promise版)
getRequestBody(request) {
return new Promise((resolve, reject) => {
let body = '';
request.on('data', (chunk) => {
body += chunk.toString();
});
request.on('end', () => {
try {
const data = body ? JSON.parse(body) : {};
resolve(data);
} catch (error) {
reject(new Error('Invalid JSON format'));
}
});
request.on('error', (error) => {
reject(error);
});
});
}
// JSONレスポンスの送信
sendJSON(response, statusCode, data) {
response.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods':
'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type',
});
response.end(JSON.stringify(data, null, 2));
}
// エラーレスポンスの送信
sendError(response, statusCode, message) {
this.sendJSON(response, statusCode, {
error: true,
message: message,
timestamp: new Date().toISOString(),
});
}
// ユーザー一覧の取得
async getUsers(request, response, query) {
// クエリパラメータによるフィルタリング
let filteredUsers = this.users;
if (query.name) {
filteredUsers = filteredUsers.filter((user) =>
user.name.includes(query.name)
);
}
if (query.minAge) {
filteredUsers = filteredUsers.filter(
(user) => user.age >= parseInt(query.minAge)
);
}
this.sendJSON(response, 200, {
users: filteredUsers,
total: filteredUsers.length,
query: query,
});
}
// 特定ユーザーの取得
async getUser(request, response, userId) {
const user = this.users.find(
(u) => u.id === parseInt(userId)
);
if (!user) {
this.sendError(
response,
404,
`ユーザー ID ${userId} が見つかりません`
);
return;
}
this.sendJSON(response, 200, { user: user });
}
// ユーザーの作成
async createUser(request, response) {
try {
const userData = await this.getRequestBody(request);
// バリデーション
if (!userData.name || !userData.email) {
this.sendError(
response,
400,
'name と email は必須項目です'
);
return;
}
// 新しいユーザーの作成
const newUser = {
id: this.nextId++,
name: userData.name,
email: userData.email,
age: userData.age || 0,
created_at: new Date().toISOString(),
};
this.users.push(newUser);
this.sendJSON(response, 201, {
message: 'ユーザーが作成されました',
user: newUser,
});
} catch (error) {
this.sendError(response, 400, error.message);
}
}
// ユーザーの更新
async updateUser(request, response, userId) {
try {
const userIndex = this.users.findIndex(
(u) => u.id === parseInt(userId)
);
if (userIndex === -1) {
this.sendError(
response,
404,
`ユーザー ID ${userId} が見つかりません`
);
return;
}
const updateData = await this.getRequestBody(request);
// ユーザー情報の更新
this.users[userIndex] = {
...this.users[userIndex],
...updateData,
updated_at: new Date().toISOString(),
};
this.sendJSON(response, 200, {
message: 'ユーザー情報が更新されました',
user: this.users[userIndex],
});
} catch (error) {
this.sendError(response, 400, error.message);
}
}
// ユーザーの削除
async deleteUser(request, response, userId) {
const userIndex = this.users.findIndex(
(u) => u.id === parseInt(userId)
);
if (userIndex === -1) {
this.sendError(
response,
404,
`ユーザー ID ${userId} が見つかりません`
);
return;
}
const deletedUser = this.users.splice(userIndex, 1)[0];
this.sendJSON(response, 200, {
message: 'ユーザーが削除されました',
deleted_user: deletedUser,
});
}
// リクエストルーティング
async handleRequest(request, response) {
const parsedUrl = url.parse(request.url, true);
const pathname = parsedUrl.pathname;
const method = request.method;
const query = parsedUrl.query;
console.log(`${method} ${pathname}`);
// OPTIONSリクエスト(CORS プリフライト)
if (method === 'OPTIONS') {
response.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods':
'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type',
});
response.end();
return;
}
// ルーティング
if (pathname === '/api/users' && method === 'GET') {
await this.getUsers(request, response, query);
} else if (
pathname.match(/^\/api\/users\/\d+$/) &&
method === 'GET'
) {
const userId = pathname.split('/')[3];
await this.getUser(request, response, userId);
} else if (
pathname === '/api/users' &&
method === 'POST'
) {
await this.createUser(request, response);
} else if (
pathname.match(/^\/api\/users\/\d+$/) &&
method === 'PUT'
) {
const userId = pathname.split('/')[3];
await this.updateUser(request, response, userId);
} else if (
pathname.match(/^\/api\/users\/\d+$/) &&
method === 'DELETE'
) {
const userId = pathname.split('/')[3];
await this.deleteUser(request, response, userId);
} else if (pathname === '/') {
// API ドキュメントページ
response.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
});
response.end(`
<h1>Node.js JSON API サーバー</h1>
<h2>利用可能なエンドポイント:</h2>
<ul>
<li><strong>GET /api/users</strong> - ユーザー一覧取得</li>
<li><strong>GET /api/users/:id</strong> - 特定ユーザー取得</li>
<li><strong>POST /api/users</strong> - ユーザー作成</li>
<li><strong>PUT /api/users/:id</strong> - ユーザー更新</li>
<li><strong>DELETE /api/users/:id</strong> - ユーザー削除</li>
</ul>
<h3>使用例:</h3>
<pre>
# ユーザー一覧取得
curl http://localhost:3000/api/users
# ユーザー作成
curl -X POST http://localhost:3000/api/users \\
-H "Content-Type: application/json" \\
-d '{"name": "新規ユーザー", "email": "new@example.com", "age": 28}'
# ユーザー更新
curl -X PUT http://localhost:3000/api/users/1 \\
-H "Content-Type: application/json" \\
-d '{"age": 31}'
</pre>
`);
} else {
this.sendError(
response,
404,
'エンドポイントが見つかりません'
);
}
}
}
// サーバーの作成と起動
const apiServer = new JSONAPIServer();
const server = http.createServer((request, response) => {
apiServer.handleRequest(request, response);
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(
`JSON API サーバー起動: http://localhost:${PORT}`
);
console.log('API ドキュメント: http://localhost:3000');
});
まとめ
この記事では、Node.js の組み込み http
モジュールを使って、段階的に HTTP サーバーを構築する方法を学習しました。
学習内容 | 習得できる技術 | 実用性 |
---|---|---|
HTTP サーバーの基礎 | Web 通信の仕組み理解 | フレームワーク使用時の理解度向上 |
リクエスト・レスポンス処理 | HTTP プロトコルの詳細操作 | カスタムミドルウェア開発 |
ルーティングシステム | URL パターンマッチング | 軽量 API サーバー構築 |
静的ファイル配信 | ファイルシステム操作 | シンプルな Web サイト構築 |
JSON API 実装 | RESTful API 設計 | 本格的な Web API 開発 |
今後の発展方向
組み込みモジュールでの基礎を理解したら、以下のステップで発展させていくことをおすすめします。
パフォーマンスの向上
- ストリーミング処理の活用
- キャッシュ機能の実装
- 圧縮機能の追加
セキュリティの強化
- HTTPS 対応
- 認証・認可システム
- 入力値検証の徹底
開発効率の向上
- Express.js などのフレームワーク導入
- TypeScript での型安全性確保
- テスト自動化の実装
Node.js の HTTP サーバー開発スキルは、モダンな Web 開発の基盤となる重要な技術です。基礎をしっかりと理解することで、より高度なフレームワークやライブラリを効果的に活用できるようになるでしょう。