Node.jsは、標準のhttpモジュールだけでWebサーバーを作れます。フレームワークを入れなくても、数行でリクエストを受け取って応答を返せるのが特徴です。仕組みを理解しておくと、Expressなどのフレームワークの中身も分かりやすくなります。
つまずきやすいのは、応答を返すres.end()を呼び忘れるとリクエストがいつまでも終わらない(ハングする)ことと、POSTで送られたデータは少しずつ届く(ストリーム)ため、まとめて受け取る書き方が必要なことです。この記事では、実機でサーバーを動かしながら、リクエスト処理・ルーティング・JSON返却までを整理します。
http.createServer((req, res) => {...})でサーバーを作り、server.listen(3000)で待ち受けます。- 応答は必ず
res.end()で終わらせます。呼ばないとリクエストがハングします。 - リクエストの内容は
req.url(パス)とreq.method(GET/POSTなど)で判定します。 - JSONを返すには、
Content-Type: application/jsonを付けてJSON.stringifyした文字列をres.end()します。 - POSTのボディは
req.on("data")とreq.on("end")で少しずつ受け取って結合します。 - 本格的なアプリでは、ルーティングを簡単に書けるExpressなどのフレームワークがよく使われます。
httpモジュールの読み込み方はrequireとimportの違い、ファイルを返すならfsでファイルを読み書きする方法、Expressの導入はnpmとpackage.jsonの基礎もあわせて参考になります。
最小限のWebサーバーを作る
まずは、アクセスすると文字を返すだけの最小のサーバーです。createServerに「リクエストが来たときの処理」を渡し、listenでポートを指定して待ち受けます。
const http = require("http");
const server = http.createServer((req, res) => {
// ヘッダー(ステータスとContent-Type)を書く
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
// 応答を返して終了する
res.end("こんにちは、サーバーです");
});
server.listen(3000, () => {
console.log("http://localhost:3000 で待ち受け中");
});
このファイルをnode server.jsで実行し、ブラウザでhttp://localhost:3000を開くと「こんにちは、サーバーです」と表示されます。charset=utf-8を付けているのは、日本語が文字化けしないようにするためです。実機でも、この形のサーバーが正しく応答を返すことを確認しました。
【重要】res.end()を呼ばないとハングする
Webサーバーで最初にハマるのがこれです。リクエストの処理の中でres.end()を呼ばないと、ブラウザは「応答待ち」のまま固まり、いつまでも読み込み中になります。
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(200);
// res.end() を呼んでいない!
// → ブラウザは応答待ちのまま固まる(ハング)
});
// 正しくは、すべての経路で必ず res.end() を呼ぶ
const ok = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("OK"); // ← これが必須
});
リクエストの処理は、必ずres.end()で締めくくる必要があります。ifで分岐したときに、一部の経路だけres.end()を呼び忘れると、その経路に来たリクエストだけがハングします。実機で確認したサーバーでも、各分岐の最後にres.end()を置いてreturnすることで、確実に応答を返せました。「ブラウザが読み込み中で止まる」ときは、res.end()の呼び忘れをまず疑ってください。
リクエストの情報を取得する
どのページへのアクセスか、どんな種類のリクエストかは、reqオブジェクトから分かります。よく使うのはreq.url(パス)とreq.method(HTTPメソッド)です。
const http = require("http");
const server = http.createServer((req, res) => {
console.log(req.method); // GET, POST など
console.log(req.url); // /, /api/user, /about など
console.log(req.headers); // リクエストヘッダー(オブジェクト)
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end(`${req.method} ${req.url} を受け取りました`);
});
server.listen(3000);
req.urlには、/api/user?id=5のようにクエリ文字列も含まれます。パスとクエリを分けて扱いたいときは、new URL(req.url, "http://localhost")のようにURLオブジェクトを使うと、pathnameやsearchParamsで取り出せます。
URLとメソッドでルーティングする
アクセス先(URL)とメソッドによって処理を分けるのが「ルーティング」です。httpモジュールには専用の仕組みが無いため、ifやswitchで自分で振り分けます。
const http = require("http");
const server = http.createServer((req, res) => {
if (req.url === "/" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("トップページ");
return;
}
if (req.url === "/about" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Aboutページ");
return;
}
// どれにも当てはまらない → 404
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("見つかりません");
});
server.listen(3000);
実機でも、/へのGETは200でトップページ、登録していない/nothereへのアクセスは404で「見つかりません」を返しました。各分岐の最後にreturnを置くことで、後ろの処理に進まないようにしています。ルートが増えるとifが長くなるため、後述のExpressのようなフレームワークが便利になります。
JSONを返す
APIとしてデータを返すなら、JSONを使います。ヘッダーにContent-Type: application/jsonを指定し、オブジェクトをJSON.stringifyで文字列にして返します。
const http = require("http");
const server = http.createServer((req, res) => {
if (req.url === "/api/user" && req.method === "GET") {
const user = { name: "山田", age: 30 };
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(user)); // オブジェクトを文字列にして返す
return;
}
res.writeHead(404);
res.end();
});
server.listen(3000);
実機でも、/api/userへのGETはContent-Type: application/json付きで{"name":"山田","age":30}を返しました。Content-Typeを付けないと、受け取る側がJSONだと判断できず、ただの文字列として扱われることがあります。res.endに渡すのは文字列なので、オブジェクトは必ずJSON.stringifyで変換します。
POSTのリクエストボディを受け取る
POSTで送られたデータ(リクエストボディ)は、少しずつ(チャンク単位で)届くため、そのままでは読めません。req.on("data")でかけらを集め、req.on("end")で全部そろってから処理します。
const http = require("http");
const server = http.createServer((req, res) => {
if (req.url === "/api/echo" && req.method === "POST") {
let body = "";
// データが届くたびに少しずつ結合する
req.on("data", (chunk) => {
body += chunk;
});
// すべて届いたら処理する
req.on("end", () => {
const data = JSON.parse(body); // 受け取ったJSONを解析
res.writeHead(201, { "Content-Type": "application/json" });
res.end(JSON.stringify({ received: data.msg }));
});
return;
}
res.writeHead(404);
res.end();
});
server.listen(3000);
実機でも、{ "msg": "こんにちは" }をPOSTすると、req.on("data")とreq.on("end")でボディを受け取り、201で{"received":"こんにちは"}を返せました。reqは読み取り用のストリームなので、この「少しずつ受け取って結合する」書き方が基本です。なお、巨大なデータでは上限を設けるなどの対策も必要になります。Expressなどのフレームワークでは、この処理をミドルウェアが代わりにやってくれます。
ステータスコードとヘッダー
応答のステータスコードとヘッダーは、res.writeHeadでまとめて指定するか、個別に設定できます。代表的なステータスコードを押さえておきましょう。
// まとめて指定する
res.writeHead(200, {
"Content-Type": "application/json",
"Cache-Control": "no-store"
});
// 個別に設定することもできる
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Not Found");
// よく使うステータスコード
// 200 OK(成功)
// 201 Created(作成成功)
// 400 Bad Request(リクエストが不正)
// 404 Not Found(見つからない)
// 500 Internal Server Error(サーバー側のエラー)
res.writeHeadは、ヘッダーを書き始めると変更できなくなります。res.setHeaderでヘッダーを設定する場合は、res.writeHeadや最初のres.writeより前に行ってください。順序を間違えると「ヘッダーを設定できない」というエラーになります。
httpモジュールとExpressの違い
標準のhttpモジュールは低レベルで、ルーティングやボディの解析を自分で書く必要があります。実際のアプリ開発では、これらを簡単に書けるフレームワーク(Expressが代表的)がよく使われます。
// Express を使うと、ルーティングがすっきり書ける
const express = require("express");
const app = express();
app.use(express.json()); // JSONボディを自動で解析
app.get("/api/user", (req, res) => {
res.json({ name: "山田", age: 30 }); // JSON返却も1行
});
app.post("/api/echo", (req, res) => {
res.status(201).json({ received: req.body.msg }); // ボディも自動
});
app.listen(3000);
Expressでは、ルーティングがapp.get・app.postで書け、JSONボディの解析(express.json())やJSON返却(res.json)も短く書けます。Expressはnpm install expressで導入します。まずはhttpモジュールで仕組みを理解し、本格的に作るときはExpressへ、という流れがおすすめです。
よくある失敗
res.end()を呼び忘れてリクエストが固まる
すべての分岐で必ずres.end()を呼びます。一部の経路で呼び忘れると、その経路だけハングします。
JSONをstringifyせずにres.endへ渡す
res.endに渡すのは文字列です。オブジェクトはJSON.stringifyで変換し、Content-Type: application/jsonも付けます。
POSTボディをreq.bodyで読もうとする
素のhttpモジュールにreq.bodyはありません。req.on("data")とreq.on("end")で結合して読みます。req.bodyはExpressなどが用意するものです。
ポートが使用中でEADDRINUSEになる
すでに同じポートを使っているプロセスがあると、EADDRINUSEになります。前のサーバーを止めるか、別のポートを指定します。
writeHeadの後にヘッダーを変えようとする
ヘッダーは送信を始めると変更できません。setHeaderはwriteHeadや最初のwriteより前に行います。
よくある質問
res.end()を呼び忘れている可能性が高いです。ifで分岐している場合は、すべての経路の最後にres.end()があるか確認してください。res.writeHead(200, { "Content-Type": "application/json" })でヘッダーを付け、res.end(JSON.stringify(オブジェクト))で返します。res.endには文字列を渡すため、オブジェクトはJSON.stringifyで変換します。req.on("data")でかけらを集め、req.on("end")ですべて届いてから処理します。素のhttpモジュールにはreq.bodyがないため、自分で結合します。Expressならexpress.json()が自動でやってくれます。httpモジュールで十分です。ルーティングやボディ解析が増える本格的なアプリでは、短く書けるExpressなどのフレームワークがおすすめです。まずhttpで基礎を理解しておくと、フレームワークも使いこなしやすくなります。listenで別のポート番号を指定してください。まとめ
http.createServer((req, res) => {...})とserver.listen(3000)でサーバーを作ります。- 応答は必ず
res.end()で終わらせます。呼び忘れるとハングします。 - ルーティングは
req.urlとreq.methodで自分で振り分けます。 - JSONは
Content-Type: application/jsonを付けてJSON.stringifyした文字列を返します。 - POSTボディは
req.on("data")とreq.on("end")で結合して受け取ります。 - 本格的なアプリでは、Expressなどのフレームワークでルーティングを簡潔に書けます。
Node.jsのWebサーバーは、httpモジュールだけでも数行で作れます。res.end()を必ず呼ぶこと、POSTボディはストリームで受け取ること、この2点を押さえれば、APIやちょっとしたサーバーをすぐに書けるようになります。仕組みを理解したら、Expressへ進むとさらに快適です。
