Skip to main content

2024年版:開発者必見のNode.jsバックエンド実例

著者

2024年6月12日

0 分で読めます

2024年もNode.jsバックエンド開発は、スケーラブルで効率的なアプリケーションを構築できる、強力で柔軟なランタイムとして注目されています。Bunなどの新たなランタイムの登場により、その存在感はさらに高まっています。

この記事では、高度なJavaScriptとNode.jsの機能を効果的に活用する、Node.jsバックエンドの基本的な実例を手軽に紹介します。WHATWG Streams StandardやWeb Streams APIを活用した効率的なデータ処理から、セキュリティのための組み込みNode.js cryptoモジュールの利用、バイナリデータ操作のためのBuffer、カプセル化と名前空間のためのSymbol、動的なHTMLやSQLクエリの生成に使うテンプレートリテラルやタグ付きテンプレートまで、各セクションで実用的なコードスニペットと実例に基づく知見を紹介します。

これらの例は、バックエンドの課題を解決するNode.jsの柔軟性と強みを示すだけでなく、2024年にバックエンドソリューションをさらに進化させたい開発者にとって、役立つリファレンスにもなります。

この記事の内容:

  1. WHATWG Streams Standard、Web Streams API、非同期イテラブル

  2. Cryptoモジュールを使ったWebhook署名の検証

  3. Node.jsでのBufferと生のバイナリデータの扱い方

  4. JavaScriptのSymbolを使ったカプセル化

  5. テンプレートリテラルとタグ付きテンプレートによるHTMLおよびSQLクエリの生成

Node.jsバックエンド実例のソースコード全体は、いつでもこちらのGitHubリポジトリで確認できます。

1. WHATWG Streams Standard、Web Streams API、非同期イテラブル

2024年は、ストリームを扱うケースが大幅に増えています。特に、生成AI(GenAI)のための大規模言語モデルを扱う場合に顕著です。チャット補完のためのOpenAI SDKは、その代表例です。OpenAI SDK APIからデータをストリーミングする一般的な例は、次のようになります。

const completion = await openai.chat.completions.create(config);

for await (const chunk of completion) {
  console.log(chunk);
}

ここでは、非同期イテラブルの概念を利用しています。これにより、Node.jsの非同期ワークフローが簡素化され、Promiseベースのプログラミングスタイルに合うようになりました。`for await…`文は、非同期イテラブルオブジェクトだけでなく、組み込みのString、Array、配列風オブジェクト(argumentsやNodeListなど)、TypedArray、Map、Set、ユーザー定義の非同期/同期イテラブルを含む同期イテラブルも反復処理するループを作成します。

Node.jsにおけるWHATWG標準のWeb Streamsの導入

WHATWG Streams標準の一部であるWeb StreamsはNode.jsに統合されており、ストリーミングデータを堅牢に処理する方法を提供します。この標準を使えば、JavaScriptでストリーミングデータを効率よく読み取り、書き込み、変換できます。

Node.jsでWeb Streamsを使う方法を、完全な例で見てみましょう。

次の例では、Web Streams APIの`ReadableStream`クラスを使ってファイルからデータストリームを作成する関数`createReadableStreamFromFile()`を定義します。2つ目の関数`consumeStreamWithAsyncIterator()`は、Async Iteratorを使ってこのストリームを消費します。

import { ReadableStream } from "node:stream/web";
import fs from "fs";

function createReadableStreamFromFile(filePath) {
  const stream = new ReadableStream({
    start(controller) {
      const reader = fs.createReadStream(filePath);

      reader.on("data", (chunk) => {
        controller.enqueue(chunk);
        if (reader.readableFlowing === false) {
          reader.resume();
        }
      });

      reader.on("end", () => {
        controller.close();
      });

      reader.on("error", (err) => {
        controller.error(err);
      });
    },
  });

  return stream;
}

async function consumeStreamWithAsyncIterator(stream) {
  try {
    for await (const chunk of stream) {
      process.stdout.write(chunk);
    }
  } catch (err) {
    console.error("Error occurred while reading the stream:", err);
  }
}

const filePath = process.argv[2];
const stream = createReadableStreamFromFile(filePath);
consumeStreamWithAsyncIterator(stream);

このコードでは、Web Streams APIを使ってファイルからデータをストリーミング形式で読み取っています。関数`createReadableStreamFromFile()`は、指定されたファイルパスから`ReadableStream`オブジェクトを返します。次に、このストリームを`consumeStreamWithAsyncIterator()`が消費し、チャンクごとに読み取って標準出力に書き込みます。読み取り中にエラーが発生した場合は、捕捉してコンソールに記録します。

ところで、上のコードにセキュリティ脆弱性があることに気づきましたか?

Snyk VS Code extensionをインストールしていれば、コード例にパストラバーサルの脆弱性があることを示す波線付きのリンターエラーが表示されます。

Snyk VS Code extensionは、安全でないデータがこのNode.jsバックエンドのコード例にどのように流れ込むか、脆弱性の内容、そして稼働中のオープンソースプロジェクトに基づくAIの提案を使った修正方法を示します。

SnykのVS Code拡張機能が、Node.jsバックエンドのコード例にセキュリティ脆弱性を検出しています。

2. Cryptoモジュールを使ったWebhook署名の検証

Webhookは現代のバックエンド開発に不可欠な要素となっており、さまざまなサービスが効率よくリアルタイムで相互に通信する手段を提供します。しかし、Webhookの利用が増えるにつれて、セキュリティ対策の強化も必要になっています。その一つがWebhook署名の検証です。

Webhookで使われる署名とは、ペイロードとともに送信されるハッシュ値です。送信者と受信者だけが知る秘密鍵を使って計算されます。受信者は自身でもハッシュ値を計算し、署名と照合することでWebhookの真正性を検証できます。

Node.jsのCryptoモジュールは、OpenSSLのhash、HMAC、cipher、decipher、sign、verify関数のラッパーなど、さまざまな暗号機能を提供します。HMAC(ハッシュベースメッセージ認証コード)は、Webhook署名の検証に特に役立ちます。

コードスニペットを分解して、動作を確認してみましょう。

const crypto = require("node:crypto");

const secret = process.env.WEBHOOK_SECERT;
const hmac = crypto.createHmac("sha256", secret);
const digest = Buffer.from(
  hmac.update(request.rawBody).digest("hex"),
  "utf8"
);
const signature = Buffer.from(request.headers["x-signature"] || "", "utf8");

if (!crypto.timingSafeEqual(digest, signature)) {
  throw new Error("Invalid signature.");
}

このスニペットでは、`crypto.createHmac`メソッドを使ってHMACオブジェクトを作成します。このメソッドは、使用するアルゴリズム(ここでは`sha256`)と秘密鍵の2つのパラメーターを受け取ります。

次に、`hmac.update`メソッドを使い、Webhookリクエストの生のボディでHMACオブジェクトを更新します。データのストリーミング中に、新しいデータを使ってこのメソッドを複数回呼び出すことができます。

続いて`digest`メソッドでハッシュ値を生成します。このメソッドはHMACオブジェクトに対して一度だけ呼び出すことができ、計算されたハッシュ値を返します。`hex`パラメーターを指定すると、ハッシュ値が16進数形式で返されます。

ここで、安全な文字列比較の概念を紹介します。計算したハッシュ値(ダイジェスト)と、Webhookリクエストヘッダーから受け取った署名は、`crypto.timingSafeEqual`メソッドで比較します。このメソッドは、2つのバッファをタイミング攻撃に強い方法で比較するため、暗号出力の比較に適しています。

タイミング攻撃とは、暗号アルゴリズムの実行にかかる時間を分析し、システムへの侵入を試みるサイドチャネル攻撃の一種です。`crypto.timingSafeEqual`のようなタイミングセーフなメソッドを使うことで、こうした攻撃を防ぎます。ダイジェストと署名が一致しない場合はエラーがスローされ、Webhookリクエストが正規のものではない可能性を示します。

3. Node.jsでのBufferと生のバイナリデータの扱い方

Node.jsのBuffer APIは、開発者がバイナリデータを直接扱える強力なツールです。ファイルの読み取り、画像の分析、生データの処理など、さまざまなタスクを効率よく実行するためのメソッドが用意されています。

Node.jsでよく使われるBuffer APIのメソッドとして、`.from`、`.alloc`、`.write`を見てみましょう。これらを使うと、バッファオブジェクトを作成・操作できます。`.from`は引数として渡されたデータから新しいバッファを作成し、`.alloc`は指定したサイズのバッファを作成します。`.write`を使えばバッファにデータを書き込めます。また、複数のBufferインスタンスを連結する`.concat`メソッドもあります。

let buffer1 = Buffer.from('Hello, ');
let buffer2 = Buffer.from('World!');
let buffer3 = Buffer.concat([buffer1, buffer2]);

console.log(buffer3.toString());
// prints: 'Hello, World!'

Node.jsのBuffer APIを使ってOpenAI APIで画像を分析する

次のNode.jsバックエンドコードの例では、Buffer APIを使って画像ファイルを読み込み、OpenAI APIで分析します。まず、必要なモジュールをインポートし、OpenAI APIクライアントのインスタンスを作成します。

import { readFile } from "node:fs/promises";
import OpenAI from "openai";

const openai = new OpenAI();

次に、画像ファイルをバッファに読み込みます。

const imageBuffer = await readFileToBuffer(process.argv[2]);

続いて、ファイルのシグネチャを既知のPNG画像のシグネチャと照合し、画像の種類を検証します。

function isImageTypeValid(imageBuffer) {
  const pngSignature = Buffer.from([
    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
  ]);
  const fileSignature = imageBuffer.slice(0, 8);

  if (pngSignature.equals(fileSignature)) {
    return true;
  }
}

最後に、OpenAI APIを使って画像の内容を説明する代替テキストを生成します。

async function generateAltTextForImage(imageBuffer) {
  const imageInBase64 = imageBuffer.toString("base64");

  const response = await openai.chat.completions.create({
    model: "gpt-4-vision-preview",
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "What's in this image? generate a simple alt text for an image source in an HTML page",
          },
          {
            type: "image_url",
            image_url: {
              url: `data:image/png;base64,${imageInBase64}`,
            },
          },
        ],
      },
    ],
  });

  return response.choices[0];
}

Buffer APIを使う際のセキュリティ上の注意点

Buffer APIを扱う際には、いくつかのセキュリティ上の注意点があります。バイナリデータの処理が不適切だと、バッファオーバーフローやアンダーフローなどのセキュリティ脆弱性につながる可能性があります。必ず入力を検証し、エラーを適切に処理してください。

また、現在は非推奨となっている`Buffer`コンストラクターなど、潜在的なセキュリティ脆弱性にも注意してください。これを避け、`Buffer.from`や`Buffer.alloc`などのより安全な代替手段を使いましょう。

4. JavaScriptのSymbolを使ったカプセル化

SymbolはES6(ECMAScript 2015)で導入されたプリミティブ型で、一意かつ不変の識別子を表します。`Symbol()`関数で作成でき、デバッグに使える説明(文字列)を任意で指定できますが、その説明はSymbolの一意性には影響しません。Symbolは主に、継承されたプロパティを含む他のプロパティと衝突しない、一意のオブジェクトプロパティキーを作成するために使われます。そのため、プロパティ名の衝突を避けながら、オブジェクトのプライベートまたは特殊なプロパティを定義するのに適しています。

クラス内にプライベートプロパティを作成し、データをカプセル化するためにSymbolを使うNode.jsバックエンドのコード例を見てみましょう。

const _privateProperty = Symbol('privateProperty');

class MyClass {
  constructor(value) {
    this[_privateProperty] = value;
  }

  getPrivateProperty() {
    return this[_privateProperty];
  }
}

const instance = new MyClass('secret');
console.log(instance.getPrivateProperty());
// Will output 'secret'
// The _privateProperty cannot be directly accessed from outside the class

また、Symbolはオブジェクトのプロパティ列挙(`for...in`ループや`Object.keys()`など)ではアクセスできません。そのため、オブジェクトのプライベートプロパティやメソッドを擬似的に実現できます。ただし、完全にプライベートというわけではなく、`Object.getOwnPropertySymbols()`などのリフレクションメソッドを使えばアクセスできる点に注意してください。

JavaScriptには、開発者がカスタマイズできる言語内部の動作を表す、一連のwell-known Symbolが定義されています。たとえば、`Symbol.iterator`を使ってカスタムオブジェクトにイテレーターを実装できます。

const iterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
  }
};

for (const value of iterable) {
  // Logs 1 and 2
  console.log(value);
}

FastifyでのJavaScript Symbolの活用

より実践的な例として、FastifyのWebアプリケーションフレームワークと、プロジェクト内でのSymbolの使い方を見てみましょう。具体的には、Fastifyのプラグインアーキテクチャの例を紹介します。Fastifyの設計で重要な要素の一つがカプセル化機能です。プラグインを使って、分離されたアプリケーションコンテキストを作成できます。これは、名前空間の衝突が問題になり得る大規模アプリケーションの構築に不可欠です。

FastifyはSymbolを使って内部プロパティやメソッドを一意に識別し、ユーザー定義のプロパティや他のプラグインのプロパティと干渉しないようにしています。以下は、プラグインのメタデータをカプセル化するために、Fastifyのソースコードで実際に使われているSymbolをもとにした簡略例です。

const pluginMeta = Symbol('fastify.pluginMeta');

function registerPlugin(instance, plugin, options) {
  if (!plugin[pluginMeta]) {
    plugin[pluginMeta] = { options, name: plugin.name };
  }

  instance.register(plugin, options);
}

function myPlugin(instance, opts, done) {
  done();
}

registerPlugin(fastifyInstance, myPlugin, { prefix: '/api' });

上の例では、`pluginMeta`はFastifyがプラグイン関数にメタデータを付与するためのSymbolです。このメタデータには、プラグインのオプションや名前、フレームワーク内部で必要となるその他の情報が含まれます。`registerPlugin`関数を使うと、Fastifyインスタンスに登録する前にプラグインへメタデータを簡単に付与できます。このメタデータはFastifyフレームワーク内で参照できますが、プラグインの公開インターフェースやアプリケーションのグローバルスコープからは分離されています。

Fastifyのようなフレームワークで、内部メタデータにJavaScriptのSymbolを使うと、次のような利点があります。

  • カプセル化:内部の詳細がユーザー空間に漏れるのを防ぎ、公開APIをすっきりと直感的に保ちます。

  • 安全性:Symbolは通常のオブジェクトプロパティ列挙では参照できないため、プラグイン同士、またはプラグインとコアフレームワークとの間で誤って干渉するリスクを軽減します。

  • 明確さ:フレームワーク内部の仕組みと開発者向けに公開されるAPIを明確に区別できるため、Fastifyのコードベースを保守・拡張しやすくなります。

5. テンプレートリテラルとタグ付きテンプレートによるHTMLおよびSQLクエリの生成

ES6で導入されたテンプレートリテラルは、JavaScriptで文字列を作成するための、より読みやすく簡潔な構文です。すでにJavaScriptで文字列を記述したり、`hello ${name}`のように動的な式を組み込んだりする際に使っているかもしれません。文字列を連結する必要はもうありません。

Node.jsのバックエンドで、タグ付きテンプレートの形式でテンプレートリテラルを使い、HTMLやSQLクエリを生成する方法が広まりつつあります。Node.jsのバックエンドとSSRコードにおける、実践的な例を2つ紹介します。

VercelのPostgreSQLライブラリでSQLクエリを生成するタグ付きテンプレート

Node.jsでSQLクエリを扱うと、冗長でエラーが起きやすいコードになりがちです。特に、クエリに値を動的に挿入する場合はその傾向が顕著です。VercelのPostgreSQLパッケージ(`@vercel/postgres`)を使うと、テンプレートリテラルでSQLクエリをより安全かつ簡潔に記述できます。

import { sql } from '@vercel/postgres';

const jediName = 'Luke Skywalker';
const { rows } = await sql`SELECT * FROM jedis WHERE name = ${jediName};`;

SQLタグ付きテンプレートリテラル関数は、`jediName`変数をSQLクエリに安全に埋め込み、SQLインジェクション攻撃を効果的に防ぎます。

FastifyサーバーでHTMLを生成するタグ付きテンプレート

SQLクエリの場合と同様に、`fastify-html`はFastifyプラグインで、タグ付きテンプレートを使ってルートハンドラー内で直接HTMLコンテンツを生成できます。サーバーサイドレンダリング(SSR)や動的なHTMLコンテンツの生成に特に便利です(htmxの話題でしょうか?)。

import fastify from 'fastify'
import fastifyHtml from 'fastify-html'

const app = fastify()
await app.register(fastifyHtml)

app.get('/', async (req, reply) => {
  const name = req.query.name || 'World';
  return reply.html`<h1>Hello ${name}</h1>`;
});

タグ付きテンプレートリテラルとは何ですか?

テンプレートリテラルを応用したタグ付きテンプレートリテラルは、テンプレートリテラルを処理するカスタム関数(「タグ」)を定義できるJavaScriptの機能です。この関数には、テンプレートリテラルが文字列の配列として、埋め込み式が個別の引数として渡されます。そのため、最終的な文字列が作成される前に、テンプレートとその値を操作できます。

まとめ

この記事が気に入った方は、セキュリティを考慮した最新のnpmパッケージ作成のベストプラクティスやDockerでNode.jsウェブアプリケーションをコンテナ化するための10のベストプラクティスもぜひご覧ください。

セキュリティ関連の情報に関心がある方は、この記事で紹介した開発者向けセキュリティ対策を活用して、npmのサプライチェーン攻撃に対処する方法もぜひご確認ください。

最後に、無料アカウントでSnykをぜひお試しください。Node.jsのコード、依存関係、Dockerコンテナイメージの保護を始められます。

続きを読む

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

Blog

AIコーディングエージェントが不適切なアクセス制御を繰り返し実装する理由

AIコーディングエージェントは、コンパイルが通りレビューも通過する一方で、あるテナントのデータを別のテナントに公開してしまう認可ロジックを生成することがあります。不適切なアクセス制御が検出しにくい理由と、その防止策をご紹介します。