Node.jsサーバー構築で実感する純正APIのシンプルな強み

まずはこのステップを実行してみて - Node.js純正APIの強みを体感しながら、すぐ試せる改善策を知る

  1. サーバー起動後、10回以上リクエストを投げて動作ログを1つずつ確認する

    毎回の反応を把握すれば、異常やパターンにすぐ気づける

  2. createServer関数の中でレスポンスヘッダーを毎回明示的にセットする

    応答形式やCORSの不具合を3件未満に抑えやすい

  3. URLパスやクエリを5種類以上手動で組み合わせて処理を試す

    ルーティングやパラメータ取得のクセを短時間で掴める

  4. 受信データ(JSON・バイナリ)を100KB単位で分割し、全体受信と部分取得の差異を比べる

    実運用前にパフォーマンスや誤動作リスクを可視化できる

Node.jsサーバーをイチから立ち上げてみよう

NodeJSサーバーの仕組み、案外見過ごされがちだけど、実はNodeアプリケーションをイチから作る時やバグの箇所を追いかける時なんか、やっぱり理解しておくと話がスムーズになるんだよね。最近は開発現場でもExpressとかKoa、それからNestなんかのフレームワークを利用してサーバーサイドアプリケーションを作っているエンジニアがぐっと増えた印象。ま、大手企業でも個人開発者でも使い方ひとつで「何これ魔法?」って感じに便利だけどさ……同時に、その奥深い仕掛けが一枚ヴェールで覆われてる気分にもなる。思えば、その根っこのロジックまでちゃんと眺めてみたいと思ったこと、ある人も多いんじゃないかな。

ところで、「node docs」ことNode公式ドキュメントって見たことある?まあ率直な話、基礎から順番に学び直したい人にはけっこう助かる内容詰まってたりする。本稿では、初心者さん向けってわけでもないけど、できるだけ噛み砕いた形でまず最小構成のWebサーバーを書いて、その流れを解説してみるつもり(いや、自分自身も原点回帰しようとしている)。それこそ「localhost:3000」にアクセスすると「Hello world」って返してくれる――ほんとに素朴な例になるかな。

<pre><code class="language-css">javascript
import {createServer} from 'node:http';
const hostname = '127.0.0.1';
const port = 3000;
const server = createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);

createServer関数で何が起きるのか理解する

http.Serverクラスを使ってインスタンスを生成する流れだけど、実はこのクラス自体、net.Serverというさらに別のクラスを受け継いでいる。そのうえで、net.ServerのほうもEventEmitterを拡張した存在なんだよね。ん?ちょっとややこしいかもしれない。要するにEventEmitterの性質が裏で生きてるわけ。

ところで(いや、どうでもいいか)、EventEmitterといえば「イベント」を色々飛ばせるイメージだけど、ここでは特にHTTPリクエストを受けたタイミングで発火するrequestイベントに注目しようと思う。これ以外にも他にイベントは確かあった気がするけど...とりあえず今回そこはスルー。requestイベントが来る時には、「request」オブジェクトと「response」オブジェクト、この2つがセットになって引数として提供される形になる。ま、普通ここから処理が分岐していくパターン多いよね。

先ほどのサンプルコードは、大雑把に言うとラッパー的な書き方になっている。この辺の感覚、最初は馴染みにくいかも? まあ仕組みとしては単純で、新しくhttp.Serverを作って、それに対して 'request' イベントリスナーを登録する感じ。「えっ、それだけ?」とか思うだろうな……例えば下記みたいなもの:
const server = new http.Server();

server.on('request', (req, res) => {
res.end('Hello!');

ま、とりあえずここまでOKなら次行こうかな。

createServer関数で何が起きるのか理解する

http.Serverインスタンスの主なイベントを活用しよう

Server インスタンスは、たとえば **server.listen(...)** や **server.close()** に加え、**server.on('request', ...)** あるいは **server.on('connection', ...)** など、多様なメソッドやイベントに触れることができる。ちょっと考えてみてほしいんだけど、こうしたインスタンスを介してアクセスする場面って意外と頻繁だと思うんだよね。あれこれ使える機能が散りばめられているから、一つ一つ追っていくと混乱しそう…まぁいいか。実際のところ、それぞれの役割が微妙に異なっていたりもするので注意した方が良いかな、と自分は思う。

リクエストとレスポンスStreamでできることとは?

timeoutやserver.maxHeadersCountなんか、設定項目って本当にたくさんあるんだよね。まあ、とにかくこれらの設定自体は**EventEmitter**から引き継いでいる構造になっていて、ごく低レベルなイベントにもリッスンできる…という一種の「裏ワザ」っぽさもある。不思議だな…。で、サーバーがHTTPリクエストをキャッチした時、一体何がどう動いてるの?ってモヤモヤすることがあったので、自分なりに**request**オブジェクトと**response**オブジェクト、その違いや共通点についてちょっと掘り下げてみる(正直面倒だけど重要だから仕方ない)。この2つ、どっちも実は**stream**型になってるし、特に**request**は読み込み用のReadable Stream、反対に**response**は書き込み専用のWritable Streamとして設計されている。このしくみのおかげでNodeJS独自のストリーム機能を余すことなく利用できる――なんというか便利そうではある。

### ルーティング付き基本HTTPサーバ

すでに単純な受信処理はできる状態ではあるけれど、「全部まとめて処理」という雑さじゃ足りないよね、多分。個々のリクエストごとに「これはこう扱う」と振り分けたい場面が必ず来る。そのカギになるのが、例によって渡される**request**オブジェクト内の色んなメタデータたちだと思う。これには例えば、

- `req.method` - つまりHTTPメソッド(`GET`とか`POST`とか)。
- `req.url` - パスもクエリー文字列も含めたまま取得できちゃう完全URL。
- `req.headers` - まぁ全部入ったヘッダー一覧かな。
- `req.httpVersion` - HTTPバージョンがここに入ってたりする。
- `req.socket` - 実際通信しているTCPソケットそのもの。

……といったものが普通についてくるわけで。ざっくり言えばルーティング処理を作り込むには、この中身――例えばパス部分を抜き出して使う技など――を活用する感じになるかな、なんとなくだけど。ま、いいか。

リクエストとレスポンスStreamでできることとは?

HTTPリクエストのメタデータからルーティングを始めよう

Node.jsでHTTPサーバーを作っていて、ルーティングを書こうと思うとね……まあ、いきなり全部ピタッとは分からない。うーん、ちょっと考えてみて?リクエストのURLやヘッダー情報には**req.url**とか**req.headers**で触れられるはず。それで、たとえば下のJavaScriptコードでは `const path = new URL(req.url, \`http://${req.headers.host}\`).pathname` って一文があってさ。ここでリクエストされたパス部分をうまく引っ張り出してる。細かい話なんだけど、たまに書式ミスるとうまく動かなくなるよ。

その上で、その抽出したパスによって処理を振り分けできる仕組みが使える。ぶっちゃけ、このへん実務でもよく見る書き方かも。

csharp
switch (path) {
case '/': {
res.setHeader('Content-Type', 'text/html');
res.end('<h1>Hello World</h1>');
break;
}
case '/upload': {
return uploadHandler(req, res);
}
case '/posts': {
return postsHandler(req, res);
}
}


……はい。各ハンドラーでは**req.method**でHTTPメソッドごとの対応を書く感じ。やっぱREST API風になるのかな?それとも単なる内部判定だけなのか、人によって好み分かれるところじゃない?

async function postsHandler(req, res) {
switch (req.method) {
case 'POST':
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ status: 'ok' }));
break;
case 'GET':
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ status: 'ok' }));
break;
}
}

### パスパラメータ(path params)を拾うとき

そうそう……ID付きリソースみたいな、「/posts/42」みたいな可変値をパス部分から取ってきたいケースもあるでしょう。その場合だけど、受け取ったリクエストオブジェクトから生成したURLインスタンス - まあほぼ自力だね、それを手作業っぽく解析して目的の値を取り出す必要が出てくるわけだよね。この点は何度も間違えて痛感した人も多いかもしれない。「あれ、意外と自動じゃ無いんだ…?」と思ったことがある人、多い気がするなぁ。ま、とにかく大筋ではこういう道筋だと思います。

URLからパスパラメータを手動で取得する方法は?

URLのクエリパラメータを引っ張り出したいとき、searchParamsってプロパティがあるんですよね。これはURLインターフェースで用意されていて、まあ書き換えたりはできないんだけど(読み取り専用です)、そのまま使うとURLSearchParamsというオブジェクトとして返ってきます。…なんか専門的に聞こえるけど、つまり、クエリ文字列―たとえば「フィルタ」や「ソート」とか「検索」「ページネーション」、要はリクエスト内容を操作するためのキー・バリューですね―それをデコードできるわけです。

あー、その手順はコード例見た方が話が早いと思うので、一応置いておきます。
/** /posts?ids=1,2,3 */
const parsedUrl = new URL(req.url, `http://${req.headers.host

URLからパスパラメータを手動で取得する方法は?

searchParamsでクエリパラメータをラクに読み取ろう

リクエストボディの取得方法って、意外といろんなパターンがあるんだよね。まず、一番スタンダードなのが Readable Stream のデフォルトイベント、「data」イベントに注目するやり方かな。
let body = []

req.on('data', (chunk) => {
body.push(chunk)
})
こうして data イベントを拾うたびにチャンク(要はバラバラのデータ片)を配列へどんどん詰めていく感じ。それで、うーん...まとめ終わったらどうするかと言えば -
req.on('end', () => {
const fullBody = Buffer.concat(body);
const bodyStr = new TextDecoder().decode(fullBody)
const bodyJSON = JSON.parse(bodyStr)

<pre><code class="language-html">res.statusCode = 200;  
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ status: 'ok' }))
})
「end」イベントが発火した時点ですべてのチャンクが来終わるから、その瞬間を逃さず全体を連結。そのあとTextDecoderで人間が読める形(文字列)へ変換し、場合によってはそれを更にJSONとして解釈できる、みたいな流れなんだよ。ちなみに注意点だけど...受け取り側がもともと文字列 - 例えばJSONペイロードとか - って分かっている場合、最初からバッファじゃなく文字列として繋げちゃう手もあり。この辺少しコード書くと:
let body = '';javascript
req.on('data', chunk => {
body += chunk; // == body = body + chunk.toString();
const decodedChunk = new TextDecoder().decode(chunk)
...
});
・・・みたいなふうにも実装可能なんだ。ただまあ、「ま、いいか。」とちょっと思っちゃうくらい細かな差異だったりするよ。どちらにせよ重要なのは「データ片を途中で何度か拾い集めて最後につなぐ」、この本質的なところさえ意識できてればOKじゃない?

受信データ(JSON)の全体取得はどう進める?

非同期イテレーターの **for await... of** 構文を実際に使ってみると、こんな風になる。いや、案外シンプルだよね。

const chunks = []
for await (const chunk of req) {
chunks.push(chunk)
}
const fullBody = Buffer.concat(chunks)
この書き方は割と直感的かつ視線にも優しくて……頭ごなしに嫌う人も少ない印象(そりゃそうか)。それと最近だと **stream/consumers.text()**(Node.js 18以降)があるらしい。一応例を書いておくね。

javascript
import { text } from 'stream/consumers';


const body = await text(req);
console.log('Body:', body);

まあ、それに加えて **buffer()**, **json()**, **arrayBuffer()** なんかのメソッドにも頼れる場面が多いはず。Expressとか流行りのフレームワーク派なら、この手順で進めば間違いない、多分。

app.use(express.text());
app.post('/', (req, res) => {
console.log(req.body); // パース済状態、らしい
});

### リクエストストリームからバイナリデータを拾う方法

正直言うと、文字列でもバイナリでも基本やることは一緒なのさ。不思議なものだけど、リクエスト経由で受け取れる「チャンク」は常に生粋のバイナリ(つまり **Buffer** オブジェクト)として姿を現す。それゆえ最後に何ができあがるか? ――要するに、その生データをどうデコードして読み解くか次第ってことかな。例えばテキストで処理したい時には、それっぽいやり方で扱えばいいし……ああ、用途によるんだった。ま、いいか。

受信データ(JSON)の全体取得はどう進める?

バイナリ・テキスト両方のリクエストボディ処理にチャレンジしよう

**Buffer.toString()**とか、まあ**TextDecoder**なんかを使ってデコードしていく感じですね。うーん…もしこれが本当に**画像**だったら、そのままバイナリ形式のままで保存するか、とりあえず適切に処理しないとダメだと思うんですよ。けど、仮にそれが**JSON**だった場合は、一旦UTF-8で文字列としてデコードした上で、そこからさらに**JSON.parse()**を使ってオブジェクトに変換しますね……あれ、そうじゃなきゃ動かないしさ(笑)。それから、このデータがいわゆる**ファイルアップロード**になっている時には、どうも普通のやり方じゃなくて、特有の**multipart/form-data**方式で区切りを判別しつつ解析してあげる必要がありそうですね。結局どれなの?……自分でも毎回悩む…。

Node.js純正API知識がフレームワーク開発にも生きる理由

Node.jsの素のAPIで書かれたuploadHandler関数、ちょっと地味に面倒だけど、POSTメソッドの場合はリクエストの本体データをひとまとまりじゃなくて複数チャンクごとに受け取って、それら全部をひとつにまとめてからfs.promises.writeFileを使い「./uploads/image.jpg」として保存する仕様になってる。ファイルへ書き込みの途中で何か問題が発生したらErrorオブジェクト経由でしっかり分かりやすいエラー内容が投げ返される感じだね。うまく最後までいけば、HTTPステータス200&JSONレスポンスで完了通知。でもってGETメソッド時にはReadable.fromとpipeが使われてJSONレスポンス送信、ここでも同じくHTTP 200がセットされる(これはまあ想像通り)。

ところでさ、この仕組みって実はもっと賢くできたりする。具体例として、「const fileStream = fs.createWriteStream('./uploads/image.jpg')」みたいなコードを書いて、「req.pipe(fileStream)」による直接パイプライン転送に切り替えれば、中間バッファで待つ必要もなくなるので結構大きめサイズのデータでもサクサク捌けちゃうんだよ。ああ…そういう便利さ、意外と侮れない気がする。

うーん……純粋なNode.jsだけでこういうサーバを作る経験はプログラミング基礎力にはわりと効く。でも正直、大掛かりな・手間が多い開発現場では一番ラクな方法とは限らないな。ただ、Node.jsのネイティブAPIをある程度自分のモノとして理解しておくことは――どんなエンジニアにも結構不可欠だったりするんじゃないかなと思った。ま、いいか。

Related to this topic:

Comments