【Node.js】httpモジュールでWebサーバーを作る|リクエスト処理・ルーティング・JSON返却

【Node.js】httpモジュールでWebサーバーを作る|リクエスト処理・ルーティング・JSON返却 Node.js

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でポートを指定して待ち受けます。

minimal-server.js
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()を呼ばないと、ブラウザは「応答待ち」のまま固まり、いつまでも読み込み中になります。

must-end.js
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() を呼ぶ

リクエストの処理は、必ずres.end()で締めくくる必要があります。ifで分岐したときに、一部の経路だけres.end()を呼び忘れると、その経路に来たリクエストだけがハングします。実機で確認したサーバーでも、各分岐の最後にres.end()を置いてreturnすることで、確実に応答を返せました。「ブラウザが読み込み中で止まる」ときは、res.end()の呼び忘れをまず疑ってください。

リクエストの情報を取得する

どのページへのアクセスか、どんな種類のリクエストかは、reqオブジェクトから分かります。よく使うのはreq.url(パス)とreq.method(HTTPメソッド)です。

req-info.js
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オブジェクトを使うと、pathnamesearchParamsで取り出せます。

URLとメソッドでルーティングする

アクセス先(URL)とメソッドによって処理を分けるのが「ルーティング」です。httpモジュールには専用の仕組みが無いため、ifswitchで自分で振り分けます。

routing.js
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で文字列にして返します。

json-response.js
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")で全部そろってから処理します。

post-body.js
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でまとめて指定するか、個別に設定できます。代表的なステータスコードを押さえておきましょう。

status-headers.js
// まとめて指定する
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-compare.js
// 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.getapp.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の後にヘッダーを変えようとする

ヘッダーは送信を始めると変更できません。setHeaderwriteHeadや最初のwriteより前に行います。

よくある質問

Qブラウザが読み込み中のまま固まります。
Aリクエストの処理でres.end()を呼び忘れている可能性が高いです。ifで分岐している場合は、すべての経路の最後にres.end()があるか確認してください。
QJSONを返すにはどうすればいいですか?
Ares.writeHead(200, { "Content-Type": "application/json" })でヘッダーを付け、res.end(JSON.stringify(オブジェクト))で返します。res.endには文字列を渡すため、オブジェクトはJSON.stringifyで変換します。
QPOSTで送られたデータを読むには?
Areq.on("data")でかけらを集め、req.on("end")ですべて届いてから処理します。素のhttpモジュールにはreq.bodyがないため、自分で結合します。Expressならexpress.json()が自動でやってくれます。
QhttpモジュールとExpressのどちらを使うべきですか?
A仕組みの学習や、ごく小さなサーバーならhttpモジュールで十分です。ルーティングやボディ解析が増える本格的なアプリでは、短く書けるExpressなどのフレームワークがおすすめです。まずhttpで基礎を理解しておくと、フレームワークも使いこなしやすくなります。
QEADDRINUSEというエラーが出ます。
A指定したポートを別のプロセスがすでに使っています。前に起動したサーバーが残っていないか確認して止めるか、listenで別のポート番号を指定してください。

まとめ

  • http.createServer((req, res) => {...})server.listen(3000)でサーバーを作ります。
  • 応答は必ずres.end()で終わらせます。呼び忘れるとハングします。
  • ルーティングはreq.urlreq.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へ進むとさらに快適です。